Skip to content

Commit 90a1984

Browse files
committed
Add socat proxy
WebSockets currently not working on macOS, see docker/for-mac#1662
1 parent 85eacc1 commit 90a1984

File tree

3 files changed

+263
-14
lines changed

3 files changed

+263
-14
lines changed

api-client/src/test/java/de/gesellix/docker/remote/api/client/ContainerApiIntegrationTest.java

Lines changed: 120 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import de.gesellix.docker.remote.api.EngineApiClient;
1313
import de.gesellix.docker.remote.api.ExecConfig;
1414
import de.gesellix.docker.remote.api.ExecStartConfig;
15+
import de.gesellix.docker.remote.api.HostConfig;
1516
import de.gesellix.docker.remote.api.IdResponse;
1617
import de.gesellix.docker.remote.api.RestartPolicy;
1718
import de.gesellix.docker.remote.api.core.Cancellable;
@@ -22,14 +23,22 @@
2223
import de.gesellix.docker.remote.api.testutil.DisabledIfDaemonOnWindowsOs;
2324
import de.gesellix.docker.remote.api.testutil.DisabledIfNotPausable;
2425
import de.gesellix.docker.remote.api.testutil.DockerEngineAvailable;
26+
import de.gesellix.docker.remote.api.testutil.EnabledIfSupportsWebSocket;
2527
import de.gesellix.docker.remote.api.testutil.Failsafe;
2628
import de.gesellix.docker.remote.api.testutil.InjectDockerClient;
29+
import de.gesellix.docker.remote.api.testutil.SocatContainer;
2730
import de.gesellix.docker.remote.api.testutil.TarUtil;
2831
import de.gesellix.docker.remote.api.testutil.TestImage;
32+
import de.gesellix.docker.websocket.DefaultWebSocketListener;
33+
import okhttp3.Response;
34+
import okhttp3.WebSocket;
35+
import okhttp3.WebSocketListener;
2936
import okio.BufferedSink;
37+
import okio.ByteString;
3038
import okio.Okio;
3139
import okio.Sink;
3240

41+
import org.jetbrains.annotations.NotNull;
3342
import org.junit.jupiter.api.BeforeEach;
3443
import org.junit.jupiter.api.Test;
3544
import org.slf4j.Logger;
@@ -48,11 +57,15 @@
4857
import java.util.TimerTask;
4958
import java.util.UUID;
5059
import java.util.concurrent.CountDownLatch;
60+
import java.util.concurrent.ExecutorService;
61+
import java.util.concurrent.Executors;
62+
import java.util.concurrent.TimeUnit;
5163
import java.util.stream.Collectors;
5264

5365
import static de.gesellix.docker.remote.api.testutil.Constants.LABEL_KEY;
5466
import static de.gesellix.docker.remote.api.testutil.Constants.LABEL_VALUE;
5567
import static de.gesellix.docker.remote.api.testutil.Failsafe.removeContainer;
68+
import static de.gesellix.docker.websocket.WebsocketStatusCode.NORMAL_CLOSURE;
5669
import static java.time.temporal.ChronoUnit.SECONDS;
5770
import static java.util.Collections.singletonList;
5871
import static java.util.Collections.singletonMap;
@@ -220,18 +233,18 @@ public void containerArchiveInfoGetAndPut() throws IOException {
220233
assertEquals("The wind\ncaught it.\n", fileContent.replaceAll("\r", ""));
221234

222235
String testPath = testImage.isWindowsContainer()
223-
? "tmp\\test"
224-
: "/tmp/test/";
236+
? "tmp\\test"
237+
: "/tmp/test/";
225238
List<String> execCmd = testImage.isWindowsContainer()
226-
? Arrays.asList("cmd", "/C", "mkdir " + testPath)
227-
: Arrays.asList("mkdir", testPath);
239+
? Arrays.asList("cmd", "/C", "mkdir " + testPath)
240+
: Arrays.asList("mkdir", testPath);
228241

229242
containerApi.containerStart("container-archive-info-test", null);
230243
IdResponse containerExec = engineApiClient.getExecApi().containerExec(
231244
"container-archive-info-test",
232245
new ExecConfig(null, null, null, null, null, null, null,
233-
execCmd,
234-
null, null, null));
246+
execCmd,
247+
null, null, null));
235248
engineApiClient.getExecApi().execStart(
236249
containerExec.getId(),
237250
new ExecStartConfig(null, null, null));
@@ -391,8 +404,7 @@ public void run() {
391404

392405
try {
393406
wait.await();
394-
}
395-
catch (InterruptedException e) {
407+
} catch (InterruptedException e) {
396408
e.printStackTrace();
397409
}
398410

@@ -453,8 +465,7 @@ public void run() {
453465

454466
try {
455467
wait.await();
456-
}
457-
catch (InterruptedException e) {
468+
} catch (InterruptedException e) {
458469
e.printStackTrace();
459470
}
460471
Optional<Frame> anyFrame = callback.frames.stream().findAny();
@@ -760,6 +771,103 @@ public void containerStatsOnce() {
760771
removeContainer(engineApiClient, "container-stats-test");
761772
}
762773

774+
@Test
775+
@EnabledIfSupportsWebSocket
776+
public void containerAttachWebSocketNonInteractive() throws InterruptedException {
777+
SocatContainer socatContainer = new SocatContainer(engineApiClient);
778+
EngineApiClient tcpClient = socatContainer.startSocatContainer();
779+
780+
removeContainer(engineApiClient, "container-attach-ws-non-interactive-test");
781+
782+
imageApi.imageCreate(testImage.getImageName(), null, null, testImage.getImageTag(), null, null, null, null, null);
783+
784+
HostConfig hostConfig = new HostConfig();
785+
hostConfig.setAutoRemove(true);
786+
ContainerCreateRequest containerCreateRequest = new ContainerCreateRequest(
787+
null, null, null,
788+
true, true, true,
789+
null,
790+
true, true, null,
791+
null,
792+
null,
793+
null,
794+
null,
795+
testImage.getImageWithTag(),
796+
null, null, singletonList("/cat"),
797+
null, null,
798+
null,
799+
singletonMap(LABEL_KEY, LABEL_VALUE),
800+
null, null,
801+
null,
802+
hostConfig,
803+
null
804+
);
805+
containerApi.containerCreate(containerCreateRequest, "container-attach-ws-non-interactive-test");
806+
containerApi.containerStart("container-attach-ws-non-interactive-test", null);
807+
808+
ExecutorService executor = Executors.newSingleThreadExecutor();
809+
String ourMessage = "hallo welt " + UUID.randomUUID();
810+
811+
CountDownLatch messageReceived = new CountDownLatch(1);
812+
List<String> receivedMessages = new ArrayList<>();
813+
WebSocketListener listener = new DefaultWebSocketListener() {
814+
@Override
815+
public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
816+
super.onOpen(webSocket, response);
817+
executor.execute(() -> webSocket.send(ourMessage));
818+
}
819+
820+
@Override
821+
public void onFailure(@NotNull WebSocket webSocket, Throwable t, Response response) {
822+
super.onFailure(webSocket, t, response);
823+
}
824+
825+
@Override
826+
public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
827+
super.onMessage(webSocket, text);
828+
receivedMessages.add(text);
829+
messageReceived.countDown();
830+
}
831+
832+
@Override
833+
public void onMessage(@NotNull WebSocket webSocket, ByteString bytes) {
834+
super.onMessage(webSocket, bytes);
835+
receivedMessages.add(bytes.utf8());
836+
messageReceived.countDown();
837+
}
838+
839+
@Override
840+
public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
841+
super.onClosing(webSocket, code, reason);
842+
}
843+
844+
@Override
845+
public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
846+
super.onClosed(webSocket, code, reason);
847+
}
848+
};
849+
850+
WebSocket webSocket = tcpClient.getContainerApi().containerAttachWebsocket(
851+
"container-attach-ws-non-interactive-test",
852+
null, true, true, true, true, true,
853+
listener);
854+
855+
// boolean enqueued = webSocket.send(ourMessage);
856+
// assertTrue(enqueued);
857+
858+
Duration timeout = Duration.of(5, SECONDS);
859+
boolean success = messageReceived.await(timeout.toMillis(), TimeUnit.MILLISECONDS);
860+
861+
webSocket.close(NORMAL_CLOSURE.getCode(), "cleanup");
862+
socatContainer.stopSocatContainer();
863+
864+
assertTrue(success);
865+
866+
assertTrue(receivedMessages.contains(ourMessage));
867+
868+
removeContainer(engineApiClient, "container-attach-ws-non-interactive-test");
869+
}
870+
763871
@Test
764872
public void containerAttachNonInteractive() {
765873
removeContainer(engineApiClient, "container-attach-non-interactive-test");
@@ -809,8 +917,7 @@ public void run() {
809917

810918
try {
811919
wait.await();
812-
}
813-
catch (InterruptedException e) {
920+
} catch (InterruptedException e) {
814921
e.printStackTrace();
815922
}
816923

@@ -893,8 +1000,7 @@ public void run() {
8931000

8941001
try {
8951002
wait.await();
896-
}
897-
catch (InterruptedException e) {
1003+
} catch (InterruptedException e) {
8981004
e.printStackTrace();
8991005
}
9001006
assertSame(Frame.StreamType.RAW, callback.frames.stream().findAny().get().getStreamType());
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package de.gesellix.docker.remote.api.testutil;
2+
3+
import static org.junit.jupiter.api.extension.ConditionEvaluationResult.disabled;
4+
import static org.junit.jupiter.api.extension.ConditionEvaluationResult.enabled;
5+
6+
import java.lang.annotation.ElementType;
7+
import java.lang.annotation.Retention;
8+
import java.lang.annotation.RetentionPolicy;
9+
import java.lang.annotation.Target;
10+
11+
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
12+
import org.junit.jupiter.api.extension.ExecutionCondition;
13+
import org.junit.jupiter.api.extension.ExtendWith;
14+
import org.junit.jupiter.api.extension.ExtensionContext;
15+
16+
import de.gesellix.docker.engine.DockerClientConfig;
17+
18+
@Target(ElementType.METHOD)
19+
@Retention(RetentionPolicy.RUNTIME)
20+
@ExtendWith(EnabledIfSupportsWebSocket.WebSocketCondition.class)
21+
public @interface EnabledIfSupportsWebSocket {
22+
23+
class WebSocketCondition implements ExecutionCondition {
24+
25+
public WebSocketCondition() {
26+
}
27+
28+
@Override
29+
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
30+
return supportsWebSocket()
31+
? enabled("Enabled: WebSockets supported")
32+
: disabled("Disabled: WebSockets not supported");
33+
}
34+
35+
public static boolean isUnixSocket() {
36+
String dockerHost = new DockerClientConfig().getEnv().getDockerHost();
37+
return dockerHost.startsWith("unix://");
38+
}
39+
40+
static boolean isTcpSocket() {
41+
String dockerHost = new DockerClientConfig().getEnv().getDockerHost();
42+
return dockerHost.startsWith("tcp://") || dockerHost.startsWith("http://") || dockerHost.startsWith("https://");
43+
}
44+
45+
private static boolean supportsWebSocket() {
46+
if (System.getProperty("os.name").toLowerCase().contains("mac")) {
47+
// currently not working on macOS
48+
// see https://github.com/docker/for-mac/issues/1662
49+
return false;
50+
}
51+
return isTcpSocket() || isUnixSocket();
52+
}
53+
}
54+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package de.gesellix.docker.remote.api.testutil;
2+
3+
import static de.gesellix.docker.remote.api.testutil.Constants.LABEL_KEY;
4+
import static de.gesellix.docker.remote.api.testutil.Constants.LABEL_VALUE;
5+
import static java.util.Collections.singletonList;
6+
import static java.util.Collections.singletonMap;
7+
8+
import de.gesellix.docker.engine.DockerClientConfig;
9+
import de.gesellix.docker.remote.api.ContainerCreateRequest;
10+
import de.gesellix.docker.remote.api.ContainerCreateResponse;
11+
import de.gesellix.docker.remote.api.ContainerInspectResponse;
12+
import de.gesellix.docker.remote.api.EngineApiClient;
13+
import de.gesellix.docker.remote.api.EngineApiClientImpl;
14+
import de.gesellix.docker.remote.api.HostConfig;
15+
16+
public class SocatContainer {
17+
18+
private final EngineApiClient engineApiClient;
19+
private final String repository;
20+
private final String tag;
21+
22+
public SocatContainer(EngineApiClient engineApiClient) {
23+
this.engineApiClient = engineApiClient;
24+
this.repository = "gesellix/socat";
25+
this.tag = "os-linux";
26+
}
27+
28+
public EngineApiClient getEngineApiClient() {
29+
return engineApiClient;
30+
}
31+
32+
public void stopSocatContainer() {
33+
engineApiClient.getContainerApi().containerStop("socat", null);
34+
}
35+
36+
public EngineApiClient startSocatContainer() {
37+
if (!EnabledIfSupportsWebSocket.WebSocketCondition.isUnixSocket()) {
38+
return engineApiClient;
39+
}
40+
// use a socat "tcp proxy" to test the websocket communication
41+
engineApiClient.getImageApi().imageCreate(getImageName(), null, null, getImageTag(), null, null, null, null, null);
42+
HostConfig hostConfig = new HostConfig();
43+
hostConfig.setAutoRemove(true);
44+
hostConfig.setPublishAllPorts(true);
45+
hostConfig.setBinds(singletonList("/var/run/docker.sock:/var/run/docker.sock"));
46+
ContainerCreateRequest socatContainerConfig = new ContainerCreateRequest(
47+
null, null, null,
48+
true, true, true,
49+
null,
50+
true, true, null,
51+
null,
52+
null,
53+
null,
54+
null,
55+
getImageWithTag(),
56+
null, null, null,
57+
null, null,
58+
null,
59+
singletonMap(LABEL_KEY, LABEL_VALUE),
60+
null, null,
61+
null,
62+
hostConfig,
63+
null
64+
);
65+
ContainerCreateResponse socatContainer = engineApiClient.getContainerApi().containerCreate(socatContainerConfig, "socat");
66+
engineApiClient.getContainerApi().containerStart("socat", null);
67+
String socatId = socatContainer.getId();
68+
ContainerInspectResponse socatDetails = engineApiClient.getContainerApi().containerInspect(socatId, false);
69+
String socatContainerPort = socatDetails.getNetworkSettings().getPorts().get("2375/tcp").get(0).getHostPort();
70+
EngineApiClientImpl tcpClient = new EngineApiClientImpl(new DockerClientConfig("tcp://localhost:" + socatContainerPort));
71+
if (!tcpClient.getSystemApi().systemPing().equals("OK")) {
72+
engineApiClient.getContainerApi().containerStop("socat", null);
73+
throw new IllegalStateException("ping failed via socat");
74+
}
75+
return tcpClient;
76+
}
77+
78+
public String getImageWithTag() {
79+
return getImageName() + ":" + getImageTag();
80+
}
81+
82+
public String getImageName() {
83+
return repository;
84+
}
85+
86+
public String getImageTag() {
87+
return tag;
88+
}
89+
}

0 commit comments

Comments
 (0)