Skip to content

Commit a2becae

Browse files
committed
Add support for container wait conditions
Expose the `condition` query parameter (added in Docker Engine API v1.30) on `WaitContainerCmd`. When the parameter is omitted, we keep the daemon’s default `not-running` behaviour. Practical implications ---------------------- * **create → wait (no condition) → start** Calling `wait` right after `create` but **before** `start` returns immediately with whatever exit-code the daemon reports for a container that has never run (typically `0`). In other words, you learn nothing about the future process. * **create → start → wait (no condition)** For short-lived containers started with `--rm`, the container may be auto-removed before the client manages to issue `wait`, resulting in a *"container not found"* error. * **create → wait(condition=removed) → start** Passing the new `removed` condition eliminates both races: the wait call blocks until the container has exited *and* been removed, reliably yielding the actual exit code even for `--rm` containers.
1 parent f6f9d67 commit a2becae

File tree

5 files changed

+145
-4
lines changed

5 files changed

+145
-4
lines changed

docker-java-api/src/main/java/com/github/dockerjava/api/command/WaitContainerCmd.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import javax.annotation.CheckForNull;
44
import javax.annotation.Nonnull;
5+
import javax.annotation.Nullable;
56

67
import com.github.dockerjava.api.async.ResultCallback;
78
import com.github.dockerjava.api.exception.NotFoundException;
9+
import com.github.dockerjava.api.model.WaitContainerCondition;
810
import com.github.dockerjava.api.model.WaitResponse;
911

1012
/**
@@ -20,8 +22,20 @@ public interface WaitContainerCmd extends AsyncDockerCmd<WaitContainerCmd, WaitR
2022
WaitContainerCmd withContainerId(@Nonnull String containerId);
2123

2224
/**
23-
* @throws NotFoundException
24-
* container not found
25+
* Defaults to {@link WaitContainerCondition#NOT_RUNNING} if omitted or empty.
26+
*
27+
* @since {@link RemoteApiVersion#VERSION_1_30}
28+
*/
29+
@Nullable
30+
WaitContainerCondition getCondition();
31+
32+
/**
33+
* @since {@link RemoteApiVersion#VERSION_1_30}
34+
*/
35+
WaitContainerCmd withCondition(@Nullable WaitContainerCondition condition);
36+
37+
/**
38+
* @throws NotFoundException container not found
2539
*/
2640
@Override
2741
<T extends ResultCallback<WaitResponse>> T exec(T resultCallback);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.github.dockerjava.api.model;
2+
3+
import javax.annotation.Nonnull;
4+
5+
/**
6+
* Docker Engine API <em>wait</em> conditions (added in v1.30).
7+
*
8+
* @since {@link RemoteApiVersion#VERSION_1_30}
9+
*/
10+
public enum WaitContainerCondition {
11+
NOT_RUNNING("not-running"),
12+
NEXT_EXIT("next-exit"),
13+
REMOVED("removed");
14+
15+
private final @Nonnull String value;
16+
17+
WaitContainerCondition(@Nonnull String value) {
18+
this.value = value;
19+
}
20+
21+
public @Nonnull String getValue() {
22+
return value;
23+
}
24+
}

docker-java-core/src/main/java/com/github/dockerjava/core/command/WaitContainerCmdImpl.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
import static com.google.common.base.Preconditions.checkNotNull;
44

5+
import javax.annotation.Nullable;
6+
57
import com.github.dockerjava.api.command.WaitContainerCmd;
8+
import com.github.dockerjava.api.model.WaitContainerCondition;
69
import com.github.dockerjava.api.model.WaitResponse;
710

811
/**
@@ -15,6 +18,8 @@ public class WaitContainerCmdImpl extends AbstrAsyncDockerCmd<WaitContainerCmd,
1518

1619
private String containerId;
1720

21+
private WaitContainerCondition condition;
22+
1823
public WaitContainerCmdImpl(WaitContainerCmd.Exec exec, String containerId) {
1924
super(exec);
2025
withContainerId(containerId);
@@ -32,4 +37,16 @@ public WaitContainerCmd withContainerId(String containerId) {
3237
return this;
3338
}
3439

40+
@Nullable
41+
@Override
42+
public WaitContainerCondition getCondition() {
43+
return condition;
44+
}
45+
46+
@Override
47+
public WaitContainerCmd withCondition(@Nullable WaitContainerCondition condition) {
48+
this.condition = condition;
49+
return this;
50+
}
51+
3552
}

docker-java-core/src/main/java/com/github/dockerjava/core/exec/WaitContainerCmdExec.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.github.dockerjava.core.exec;
22

3+
import com.github.dockerjava.api.model.WaitContainerCondition;
34
import org.slf4j.Logger;
45
import org.slf4j.LoggerFactory;
56

@@ -25,6 +26,11 @@ protected Void execute0(WaitContainerCmd command, ResultCallback<WaitResponse> r
2526
WebTarget webTarget = getBaseResource().path("/containers/{id}/wait").resolveTemplate("id",
2627
command.getContainerId());
2728

29+
WaitContainerCondition condition = command.getCondition();
30+
if (condition != null) {
31+
webTarget = webTarget.queryParam("condition", condition.getValue());
32+
}
33+
2834
LOGGER.trace("POST: {}", webTarget);
2935

3036
webTarget.request().accept(MediaType.APPLICATION_JSON).post((Object) null, new TypeReference<WaitResponse>() {

docker-java/src/test/java/com/github/dockerjava/cmd/WaitContainerCmdIT.java

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,25 @@
88
import com.github.dockerjava.api.exception.DockerClientException;
99
import com.github.dockerjava.api.exception.DockerException;
1010
import com.github.dockerjava.api.exception.NotFoundException;
11+
import com.github.dockerjava.api.model.WaitContainerCondition;
1112
import com.github.dockerjava.api.model.WaitResponse;
13+
import org.junit.Assert;
1214
import org.junit.Test;
1315
import org.slf4j.Logger;
1416
import org.slf4j.LoggerFactory;
1517

1618
import java.util.concurrent.TimeUnit;
1719

20+
import static com.github.dockerjava.api.model.HostConfig.newHostConfig;
21+
import static com.github.dockerjava.core.RemoteApiVersion.VERSION_1_25;
22+
import static com.github.dockerjava.core.RemoteApiVersion.VERSION_1_30;
23+
import static com.github.dockerjava.junit.DockerMatchers.isGreaterOrEqual;
1824
import static org.hamcrest.MatcherAssert.assertThat;
1925
import static org.hamcrest.Matchers.equalTo;
2026
import static org.hamcrest.Matchers.is;
2127
import static org.hamcrest.Matchers.emptyString;
2228
import static org.hamcrest.Matchers.not;
29+
import static org.junit.Assume.assumeThat;
2330

2431
public class WaitContainerCmdIT extends CmdIT {
2532
public static final Logger LOG = LoggerFactory.getLogger(BuildImageCmd.class);
@@ -35,7 +42,7 @@ public void testWaitContainer() throws DockerException {
3542
dockerRule.getClient().startContainerCmd(container.getId()).exec();
3643

3744
int exitCode = dockerRule.getClient().waitContainerCmd(container.getId()).start()
38-
.awaitStatusCode();
45+
.awaitStatusCode();
3946
LOG.info("Container exit code: {}", exitCode);
4047

4148
assertThat(exitCode, equalTo(0));
@@ -94,12 +101,85 @@ public void testWaitContainerTimeout() throws Exception {
94101
dockerRule.getClient().startContainerCmd(container.getId()).exec();
95102

96103
WaitContainerResultCallback callback = dockerRule.getClient().waitContainerCmd(container.getId()).exec(
97-
new WaitContainerResultCallback());
104+
new WaitContainerResultCallback());
98105
try {
99106
callback.awaitStatusCode(100, TimeUnit.MILLISECONDS);
100107
throw new AssertionError("Should throw exception on timeout.");
101108
} catch (DockerClientException e) {
102109
LOG.info(e.getMessage());
103110
}
104111
}
112+
113+
@Test
114+
public void testWaitNotStartedContainer() {
115+
assumeThat("API version should be > 1.25", dockerRule, isGreaterOrEqual(VERSION_1_25));
116+
117+
CreateContainerResponse container = dockerRule.getClient().createContainerCmd("busybox")
118+
.withHostConfig(newHostConfig().withAutoRemove(true))
119+
.exec();
120+
121+
LOG.info("Created container: {}", container.toString());
122+
assertThat(container.getId(), not(is(emptyString())));
123+
124+
WaitContainerResultCallback callback = dockerRule.getClient().waitContainerCmd(container.getId()).exec(new WaitContainerResultCallback());
125+
126+
Integer statusCode = callback.awaitStatusCode(100, TimeUnit.MILLISECONDS);
127+
Assert.assertEquals(0, statusCode.intValue());
128+
}
129+
130+
@Test
131+
public void testWaitContainerWithAutoRemoval() {
132+
assumeThat("API version should be > 1.30", dockerRule, isGreaterOrEqual(VERSION_1_30));
133+
134+
CreateContainerResponse container = dockerRule.getClient().createContainerCmd("busybox")
135+
.withCmd("false")
136+
.withHostConfig(newHostConfig().withAutoRemove(true))
137+
.exec();
138+
139+
LOG.info("Created container: {}", container.toString());
140+
assertThat(container.getId(), not(is(emptyString())));
141+
142+
WaitContainerResultCallback removedCondition = dockerRule.getClient().waitContainerCmd(container.getId())
143+
.withCondition(WaitContainerCondition.REMOVED)
144+
.exec(new WaitContainerResultCallback());
145+
146+
WaitContainerResultCallback nextExitCondition = dockerRule.getClient().waitContainerCmd(container.getId())
147+
.withCondition(WaitContainerCondition.NEXT_EXIT)
148+
.exec(new WaitContainerResultCallback());
149+
150+
dockerRule.getClient().startContainerCmd(container.getId()).exec();
151+
152+
Assert.assertEquals(1, removedCondition.awaitStatusCode(100, TimeUnit.MILLISECONDS).intValue());
153+
Assert.assertEquals(1, nextExitCondition.awaitStatusCode(100, TimeUnit.MILLISECONDS).intValue());
154+
}
155+
156+
@Test
157+
public void testWaitRestartedContainer() {
158+
assumeThat("API version should be > 1.30", dockerRule, isGreaterOrEqual(VERSION_1_30));
159+
160+
CreateContainerResponse container = dockerRule.getClient().createContainerCmd("busybox")
161+
.withCmd("sh", "-c", "[ -f \"$HOME/.first_run_marker\" ] && exit 2 || { touch \"$HOME/.first_run_marker\"; exit 1; }")
162+
.exec();
163+
164+
LOG.info("Created container: {}", container.toString());
165+
assertThat(container.getId(), not(is(emptyString())));
166+
167+
WaitContainerResultCallback firstExitCallback = dockerRule.getClient().waitContainerCmd(container.getId())
168+
.withCondition(WaitContainerCondition.NEXT_EXIT)
169+
.exec(new WaitContainerResultCallback());
170+
171+
dockerRule.getClient().startContainerCmd(container.getId()).exec();
172+
173+
Integer firstStatusCode = firstExitCallback.awaitStatusCode(100, TimeUnit.MILLISECONDS);
174+
Assert.assertEquals(1, firstStatusCode.intValue());
175+
176+
WaitContainerResultCallback callback = dockerRule.getClient().waitContainerCmd(container.getId())
177+
.withCondition(WaitContainerCondition.NEXT_EXIT)
178+
.exec(new WaitContainerResultCallback());
179+
180+
dockerRule.getClient().startContainerCmd(container.getId()).exec();
181+
182+
Integer statusCode = callback.awaitStatusCode(100, TimeUnit.MILLISECONDS);
183+
Assert.assertEquals(2, statusCode.intValue());
184+
}
105185
}

0 commit comments

Comments
 (0)