Skip to content

Commit d67fffc

Browse files
committed
Add registry auth to service updates
Allow UpdateServiceCmd to forward registry credentials through the X-Registry-Auth header. Cover header serialization with focused tests and verify service updates can pull an image from an authenticated private registry.
1 parent 002b6eb commit d67fffc

6 files changed

Lines changed: 213 additions & 4 deletions

File tree

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

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

3+
import com.github.dockerjava.api.model.AuthConfig;
34
import com.github.dockerjava.api.model.ServiceSpec;
45

56
import javax.annotation.CheckForNull;
@@ -19,6 +20,12 @@ public interface UpdateServiceCmd extends SyncDockerCmd<Void> {
1920

2021
UpdateServiceCmd withServiceSpec(ServiceSpec serviceSpec);
2122

23+
@CheckForNull
24+
AuthConfig getAuthConfig();
25+
26+
@Nonnull
27+
UpdateServiceCmd withAuthConfig(@Nonnull AuthConfig authConfig);
28+
2229
@CheckForNull
2330
Long getVersion();
2431

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

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

33
import com.github.dockerjava.api.command.UpdateServiceCmd;
44
import com.github.dockerjava.api.exception.NotFoundException;
5+
import com.github.dockerjava.api.model.AuthConfig;
56
import com.github.dockerjava.api.model.ServiceSpec;
67
import com.github.dockerjava.core.RemoteApiVersion;
78
import org.apache.commons.lang3.builder.EqualsBuilder;
@@ -11,6 +12,7 @@
1112

1213
import javax.annotation.CheckForNull;
1314
import javax.annotation.Nonnull;
15+
import java.util.Objects;
1416

1517
/**
1618
* @since {@link RemoteApiVersion#VERSION_1_24}
@@ -28,6 +30,13 @@ public class UpdateServiceCmdImpl extends AbstrDockerCmd<UpdateServiceCmd, Void>
2830
*/
2931
private ServiceSpec serviceSpec;
3032

33+
/**
34+
* Registry authentication sent with the service update request.
35+
*
36+
* @since {@link RemoteApiVersion#VERSION_1_24}
37+
*/
38+
private AuthConfig authConfig;
39+
3140
/**
3241
* @since 1.24
3342
*/
@@ -71,6 +80,25 @@ public UpdateServiceCmd withServiceSpec(ServiceSpec serviceSpec) {
7180
return this;
7281
}
7382

83+
/**
84+
* @see #authConfig
85+
*/
86+
@Override
87+
@CheckForNull
88+
public AuthConfig getAuthConfig() {
89+
return authConfig;
90+
}
91+
92+
/**
93+
* @see #authConfig
94+
*/
95+
@Override
96+
@Nonnull
97+
public UpdateServiceCmd withAuthConfig(@Nonnull AuthConfig authConfig) {
98+
this.authConfig = Objects.requireNonNull(authConfig, "authConfig was not specified");
99+
return this;
100+
}
101+
74102
/**
75103
* @see #version
76104
*/

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.github.dockerjava.api.command.UpdateServiceCmd;
44
import com.github.dockerjava.core.DockerClientConfig;
5+
import com.github.dockerjava.core.InvocationBuilder;
56
import com.github.dockerjava.core.MediaType;
67
import com.github.dockerjava.core.WebTarget;
78
import org.slf4j.Logger;
@@ -27,8 +28,10 @@ protected Void execute(UpdateServiceCmd command) {
2728
.queryParam("version", command.getVersion());
2829

2930
LOGGER.trace("POST: {}", webResource);
31+
InvocationBuilder builder = resourceWithOptionalAuthConfig(command.getAuthConfig(), webResource.request())
32+
.accept(MediaType.APPLICATION_JSON);
3033
try {
31-
webResource.request().accept(MediaType.APPLICATION_JSON).post(command.getServiceSpec()).close();
34+
builder.post(command.getServiceSpec()).close();
3235
} catch (IOException e) {
3336
throw new RuntimeException(e);
3437
}

docker-java/src/test/java/com/github/dockerjava/cmd/swarm/UpdateSwarmServiceIT.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,23 @@
88
import com.github.dockerjava.api.model.ServiceModeConfig;
99
import com.github.dockerjava.api.model.ServiceReplicatedModeOptions;
1010
import com.github.dockerjava.api.model.ServiceSpec;
11+
import com.github.dockerjava.api.model.Task;
1112
import com.github.dockerjava.api.model.TaskSpec;
13+
import com.github.dockerjava.api.model.TaskState;
14+
import com.github.dockerjava.junit.PrivateRegistryRule;
1215
import com.google.common.collect.Lists;
1316
import org.junit.Test;
1417

1518
import java.util.Arrays;
1619
import java.util.List;
20+
import java.util.concurrent.TimeUnit;
1721

22+
import static com.github.dockerjava.core.DockerRule.DEFAULT_IMAGE;
1823
import static org.awaitility.Awaitility.await;
1924
import static org.hamcrest.MatcherAssert.assertThat;
25+
import static org.hamcrest.Matchers.hasSize;
2026
import static org.hamcrest.Matchers.is;
27+
import static org.hamcrest.Matchers.startsWith;
2128

2229
public class UpdateSwarmServiceIT extends SwarmCmdIT {
2330
@Test
@@ -46,4 +53,45 @@ public void testUpdateServiceReplicate() {
4653
assertThat(updateService.getSpec().getMode().getReplicated().getReplicas(), is(2L));
4754
});
4855
}
56+
57+
@Test
58+
public void testUpdateServiceWithRegistryAuth() throws InterruptedException {
59+
DockerClient dockerClient = startSwarm();
60+
ServiceSpec serviceSpec = new ServiceSpec()
61+
.withName("authenticated-worker")
62+
.withMode(new ServiceModeConfig().withReplicated(new ServiceReplicatedModeOptions().withReplicas(1)))
63+
.withTaskTemplate(new TaskSpec().withContainerSpec(
64+
new ContainerSpec().withImage(DEFAULT_IMAGE).withArgs(Arrays.asList("sleep", "3600"))));
65+
String serviceId = dockerClient.createServiceCmd(serviceSpec).exec().getId();
66+
67+
await().atMost(60, TimeUnit.SECONDS).untilAsserted(() -> {
68+
List<Task> tasks = dockerClient.listTasksCmd()
69+
.withServiceFilter(serviceId)
70+
.withStateFilter(TaskState.RUNNING)
71+
.exec();
72+
assertThat(tasks, hasSize(1));
73+
});
74+
75+
try (PrivateRegistryRule registry = new PrivateRegistryRule(dockerClient)) {
76+
registry.start();
77+
String privateImage = registry.createPrivateImage("update-service");
78+
Service service = dockerClient.inspectServiceCmd(serviceId).exec();
79+
ServiceSpec updatedServiceSpec = service.getSpec();
80+
updatedServiceSpec.getTaskTemplate().getContainerSpec().withImage(privateImage);
81+
82+
dockerClient.updateServiceCmd(serviceId, updatedServiceSpec)
83+
.withVersion(service.getVersion().getIndex())
84+
.withAuthConfig(registry.getAuthConfig())
85+
.exec();
86+
87+
await().atMost(60, TimeUnit.SECONDS).untilAsserted(() -> {
88+
List<Task> tasks = dockerClient.listTasksCmd()
89+
.withServiceFilter(serviceId)
90+
.withStateFilter(TaskState.RUNNING)
91+
.exec();
92+
assertThat(tasks, hasSize(1));
93+
assertThat(tasks.get(0).getSpec().getContainerSpec().getImage(), startsWith(privateImage));
94+
});
95+
}
96+
}
4997
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package com.github.dockerjava.core.exec;
2+
3+
import com.github.dockerjava.api.DockerClient;
4+
import com.github.dockerjava.api.model.AuthConfig;
5+
import com.github.dockerjava.api.model.ServiceSpec;
6+
import com.github.dockerjava.core.DefaultDockerClientConfig;
7+
import com.github.dockerjava.core.DockerClientConfig;
8+
import com.github.dockerjava.core.DockerClientImpl;
9+
import com.github.dockerjava.transport.DockerHttpClient;
10+
import com.google.common.io.BaseEncoding;
11+
import org.junit.Test;
12+
13+
import java.io.ByteArrayInputStream;
14+
import java.io.IOException;
15+
import java.io.InputStream;
16+
import java.util.Collections;
17+
import java.util.List;
18+
import java.util.Map;
19+
import java.util.concurrent.atomic.AtomicReference;
20+
21+
import static org.hamcrest.MatcherAssert.assertThat;
22+
import static org.hamcrest.Matchers.is;
23+
import static org.hamcrest.Matchers.notNullValue;
24+
import static org.hamcrest.Matchers.nullValue;
25+
26+
public class UpdateServiceCmdExecTest {
27+
28+
private static final String REGISTRY_AUTH_HEADER = "X-Registry-Auth";
29+
30+
@Test
31+
public void sendsRegistryAuthHeader() throws Exception {
32+
AtomicReference<DockerHttpClient.Request> request = new AtomicReference<>();
33+
DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
34+
DockerClient dockerClient = DockerClientImpl.getInstance(config, new CapturingDockerHttpClient(request));
35+
AuthConfig authConfig = new AuthConfig()
36+
.withUsername("user")
37+
.withPassword("password")
38+
.withRegistryAddress("registry.example.com");
39+
40+
dockerClient.updateServiceCmd("service-id", new ServiceSpec())
41+
.withVersion(1L)
42+
.withAuthConfig(authConfig)
43+
.exec();
44+
45+
String encodedAuth = request.get().headers().get(REGISTRY_AUTH_HEADER);
46+
assertThat(encodedAuth, notNullValue());
47+
byte[] decodedAuth = BaseEncoding.base64Url().decode(encodedAuth);
48+
AuthConfig sentAuth = config.getObjectMapper().readValue(decodedAuth, AuthConfig.class);
49+
assertThat(sentAuth, is(authConfig));
50+
}
51+
52+
@Test
53+
public void omitsRegistryAuthHeaderWhenNotConfigured() {
54+
AtomicReference<DockerHttpClient.Request> request = new AtomicReference<>();
55+
DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
56+
DockerClient dockerClient = DockerClientImpl.getInstance(config, new CapturingDockerHttpClient(request));
57+
58+
dockerClient.updateServiceCmd("service-id", new ServiceSpec())
59+
.withVersion(1L)
60+
.exec();
61+
62+
assertThat(request.get().headers().get(REGISTRY_AUTH_HEADER), nullValue());
63+
}
64+
65+
private static class CapturingDockerHttpClient implements DockerHttpClient {
66+
67+
private final AtomicReference<Request> request;
68+
69+
CapturingDockerHttpClient(AtomicReference<Request> request) {
70+
this.request = request;
71+
}
72+
73+
@Override
74+
public Response execute(Request request) {
75+
this.request.set(request);
76+
return new EmptyResponse();
77+
}
78+
79+
@Override
80+
public void close() throws IOException {
81+
}
82+
}
83+
84+
private static class EmptyResponse implements DockerHttpClient.Response {
85+
86+
@Override
87+
public int getStatusCode() {
88+
return 200;
89+
}
90+
91+
@Override
92+
public Map<String, List<String>> getHeaders() {
93+
return Collections.emptyMap();
94+
}
95+
96+
@Override
97+
public InputStream getBody() {
98+
return new ByteArrayInputStream(new byte[0]);
99+
}
100+
101+
@Override
102+
public void close() {
103+
}
104+
}
105+
}

docker-java/src/test/java/com/github/dockerjava/junit/PrivateRegistryRule.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.junit.rules.ExternalResource;
1313

1414
import java.io.File;
15+
import java.util.Objects;
1516
import java.util.concurrent.TimeUnit;
1617

1718
import static com.github.dockerjava.api.model.HostConfig.newHostConfig;
@@ -20,7 +21,7 @@
2021
import static org.hamcrest.Matchers.not;
2122
import static org.hamcrest.Matchers.nullValue;
2223

23-
public class PrivateRegistryRule extends ExternalResource {
24+
public class PrivateRegistryRule extends ExternalResource implements AutoCloseable {
2425

2526
private final DockerClient dockerClient;
2627

@@ -29,7 +30,11 @@ public class PrivateRegistryRule extends ExternalResource {
2930
private String containerId;
3031

3132
public PrivateRegistryRule() {
32-
this.dockerClient = CmdIT.createDockerClient(DockerRule.config(null));
33+
this(CmdIT.createDockerClient(DockerRule.config(null)));
34+
}
35+
36+
public PrivateRegistryRule(DockerClient dockerClient) {
37+
this.dockerClient = Objects.requireNonNull(dockerClient);
3338
}
3439

3540
public AuthConfig getAuthConfig() {
@@ -59,12 +64,20 @@ public String createTestImage(String tagName) {
5964
return imgName + ":" + tagName;
6065
}
6166

67+
public void start() throws InterruptedException {
68+
startRegistry();
69+
}
70+
6271
/**
6372
* Starts a local test registry when it is not already started and returns the auth configuration for it
6473
* This method is synchronized so that only the first invocation starts the registry
6574
*/
6675
@Override
67-
protected void before() throws Throwable {
76+
protected void before() throws InterruptedException {
77+
startRegistry();
78+
}
79+
80+
private void startRegistry() throws InterruptedException {
6881

6982
int port = 5050;
7083

@@ -116,4 +129,9 @@ protected void after() {
116129
.exec();
117130
}
118131
}
132+
133+
@Override
134+
public void close() {
135+
after();
136+
}
119137
}

0 commit comments

Comments
 (0)