diff --git a/docker-java-api/src/main/java/com/github/dockerjava/api/command/UpdateServiceCmd.java b/docker-java-api/src/main/java/com/github/dockerjava/api/command/UpdateServiceCmd.java index da4b783874..dc84c74aed 100644 --- a/docker-java-api/src/main/java/com/github/dockerjava/api/command/UpdateServiceCmd.java +++ b/docker-java-api/src/main/java/com/github/dockerjava/api/command/UpdateServiceCmd.java @@ -1,5 +1,6 @@ package com.github.dockerjava.api.command; +import com.github.dockerjava.api.model.AuthConfig; import com.github.dockerjava.api.model.ServiceSpec; import javax.annotation.CheckForNull; @@ -19,6 +20,12 @@ public interface UpdateServiceCmd extends SyncDockerCmd { UpdateServiceCmd withServiceSpec(ServiceSpec serviceSpec); + @CheckForNull + AuthConfig getAuthConfig(); + + @Nonnull + UpdateServiceCmd withAuthConfig(@Nonnull AuthConfig authConfig); + @CheckForNull Long getVersion(); diff --git a/docker-java-core/src/main/java/com/github/dockerjava/core/command/UpdateServiceCmdImpl.java b/docker-java-core/src/main/java/com/github/dockerjava/core/command/UpdateServiceCmdImpl.java index 7ff9412a96..6d96b772f9 100644 --- a/docker-java-core/src/main/java/com/github/dockerjava/core/command/UpdateServiceCmdImpl.java +++ b/docker-java-core/src/main/java/com/github/dockerjava/core/command/UpdateServiceCmdImpl.java @@ -2,6 +2,7 @@ import com.github.dockerjava.api.command.UpdateServiceCmd; import com.github.dockerjava.api.exception.NotFoundException; +import com.github.dockerjava.api.model.AuthConfig; import com.github.dockerjava.api.model.ServiceSpec; import com.github.dockerjava.core.RemoteApiVersion; import org.apache.commons.lang3.builder.EqualsBuilder; @@ -11,6 +12,7 @@ import javax.annotation.CheckForNull; import javax.annotation.Nonnull; +import java.util.Objects; /** * @since {@link RemoteApiVersion#VERSION_1_24} @@ -28,6 +30,13 @@ public class UpdateServiceCmdImpl extends AbstrDockerCmd */ private ServiceSpec serviceSpec; + /** + * Registry authentication sent with the service update request. + * + * @since {@link RemoteApiVersion#VERSION_1_24} + */ + private AuthConfig authConfig; + /** * @since 1.24 */ @@ -71,6 +80,25 @@ public UpdateServiceCmd withServiceSpec(ServiceSpec serviceSpec) { return this; } + /** + * @see #authConfig + */ + @Override + @CheckForNull + public AuthConfig getAuthConfig() { + return authConfig; + } + + /** + * @see #authConfig + */ + @Override + @Nonnull + public UpdateServiceCmd withAuthConfig(@Nonnull AuthConfig authConfig) { + this.authConfig = Objects.requireNonNull(authConfig, "authConfig was not specified"); + return this; + } + /** * @see #version */ diff --git a/docker-java-core/src/main/java/com/github/dockerjava/core/exec/UpdateServiceCmdExec.java b/docker-java-core/src/main/java/com/github/dockerjava/core/exec/UpdateServiceCmdExec.java index 61eb8b2710..893322d412 100644 --- a/docker-java-core/src/main/java/com/github/dockerjava/core/exec/UpdateServiceCmdExec.java +++ b/docker-java-core/src/main/java/com/github/dockerjava/core/exec/UpdateServiceCmdExec.java @@ -2,6 +2,7 @@ import com.github.dockerjava.api.command.UpdateServiceCmd; import com.github.dockerjava.core.DockerClientConfig; +import com.github.dockerjava.core.InvocationBuilder; import com.github.dockerjava.core.MediaType; import com.github.dockerjava.core.WebTarget; import org.slf4j.Logger; @@ -27,8 +28,10 @@ protected Void execute(UpdateServiceCmd command) { .queryParam("version", command.getVersion()); LOGGER.trace("POST: {}", webResource); + InvocationBuilder builder = resourceWithOptionalAuthConfig(command.getAuthConfig(), webResource.request()) + .accept(MediaType.APPLICATION_JSON); try { - webResource.request().accept(MediaType.APPLICATION_JSON).post(command.getServiceSpec()).close(); + builder.post(command.getServiceSpec()).close(); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/docker-java/src/test/java/com/github/dockerjava/cmd/swarm/UpdateSwarmServiceIT.java b/docker-java/src/test/java/com/github/dockerjava/cmd/swarm/UpdateSwarmServiceIT.java index c477320bfb..ca8581856b 100644 --- a/docker-java/src/test/java/com/github/dockerjava/cmd/swarm/UpdateSwarmServiceIT.java +++ b/docker-java/src/test/java/com/github/dockerjava/cmd/swarm/UpdateSwarmServiceIT.java @@ -8,16 +8,23 @@ import com.github.dockerjava.api.model.ServiceModeConfig; import com.github.dockerjava.api.model.ServiceReplicatedModeOptions; import com.github.dockerjava.api.model.ServiceSpec; +import com.github.dockerjava.api.model.Task; import com.github.dockerjava.api.model.TaskSpec; +import com.github.dockerjava.api.model.TaskState; +import com.github.dockerjava.junit.PrivateRegistryRule; import com.google.common.collect.Lists; import org.junit.Test; import java.util.Arrays; import java.util.List; +import java.util.concurrent.TimeUnit; +import static com.github.dockerjava.core.DockerRule.DEFAULT_IMAGE; import static org.awaitility.Awaitility.await; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; public class UpdateSwarmServiceIT extends SwarmCmdIT { @Test @@ -46,4 +53,45 @@ public void testUpdateServiceReplicate() { assertThat(updateService.getSpec().getMode().getReplicated().getReplicas(), is(2L)); }); } + + @Test + public void testUpdateServiceWithRegistryAuth() throws InterruptedException { + DockerClient dockerClient = startSwarm(); + ServiceSpec serviceSpec = new ServiceSpec() + .withName("authenticated-worker") + .withMode(new ServiceModeConfig().withReplicated(new ServiceReplicatedModeOptions().withReplicas(1))) + .withTaskTemplate(new TaskSpec().withContainerSpec( + new ContainerSpec().withImage(DEFAULT_IMAGE).withArgs(Arrays.asList("sleep", "3600")))); + String serviceId = dockerClient.createServiceCmd(serviceSpec).exec().getId(); + + await().atMost(60, TimeUnit.SECONDS).untilAsserted(() -> { + List tasks = dockerClient.listTasksCmd() + .withServiceFilter(serviceId) + .withStateFilter(TaskState.RUNNING) + .exec(); + assertThat(tasks, hasSize(1)); + }); + + try (PrivateRegistryRule registry = new PrivateRegistryRule(dockerClient)) { + registry.start(); + String privateImage = registry.createPrivateImage("update-service"); + Service service = dockerClient.inspectServiceCmd(serviceId).exec(); + ServiceSpec updatedServiceSpec = service.getSpec(); + updatedServiceSpec.getTaskTemplate().getContainerSpec().withImage(privateImage); + + dockerClient.updateServiceCmd(serviceId, updatedServiceSpec) + .withVersion(service.getVersion().getIndex()) + .withAuthConfig(registry.getAuthConfig()) + .exec(); + + await().atMost(60, TimeUnit.SECONDS).untilAsserted(() -> { + List tasks = dockerClient.listTasksCmd() + .withServiceFilter(serviceId) + .withStateFilter(TaskState.RUNNING) + .exec(); + assertThat(tasks, hasSize(1)); + assertThat(tasks.get(0).getSpec().getContainerSpec().getImage(), startsWith(privateImage)); + }); + } + } } diff --git a/docker-java/src/test/java/com/github/dockerjava/core/exec/UpdateServiceCmdExecTest.java b/docker-java/src/test/java/com/github/dockerjava/core/exec/UpdateServiceCmdExecTest.java new file mode 100644 index 0000000000..a82d168cf8 --- /dev/null +++ b/docker-java/src/test/java/com/github/dockerjava/core/exec/UpdateServiceCmdExecTest.java @@ -0,0 +1,105 @@ +package com.github.dockerjava.core.exec; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.model.AuthConfig; +import com.github.dockerjava.api.model.ServiceSpec; +import com.github.dockerjava.core.DefaultDockerClientConfig; +import com.github.dockerjava.core.DockerClientConfig; +import com.github.dockerjava.core.DockerClientImpl; +import com.github.dockerjava.transport.DockerHttpClient; +import com.google.common.io.BaseEncoding; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class UpdateServiceCmdExecTest { + + private static final String REGISTRY_AUTH_HEADER = "X-Registry-Auth"; + + @Test + public void sendsRegistryAuthHeader() throws Exception { + AtomicReference request = new AtomicReference<>(); + DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); + DockerClient dockerClient = DockerClientImpl.getInstance(config, new CapturingDockerHttpClient(request)); + AuthConfig authConfig = new AuthConfig() + .withUsername("user") + .withPassword("password") + .withRegistryAddress("registry.example.com"); + + dockerClient.updateServiceCmd("service-id", new ServiceSpec()) + .withVersion(1L) + .withAuthConfig(authConfig) + .exec(); + + String encodedAuth = request.get().headers().get(REGISTRY_AUTH_HEADER); + assertThat(encodedAuth, notNullValue()); + byte[] decodedAuth = BaseEncoding.base64Url().decode(encodedAuth); + AuthConfig sentAuth = config.getObjectMapper().readValue(decodedAuth, AuthConfig.class); + assertThat(sentAuth, is(authConfig)); + } + + @Test + public void omitsRegistryAuthHeaderWhenNotConfigured() { + AtomicReference request = new AtomicReference<>(); + DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); + DockerClient dockerClient = DockerClientImpl.getInstance(config, new CapturingDockerHttpClient(request)); + + dockerClient.updateServiceCmd("service-id", new ServiceSpec()) + .withVersion(1L) + .exec(); + + assertThat(request.get().headers().get(REGISTRY_AUTH_HEADER), nullValue()); + } + + private static class CapturingDockerHttpClient implements DockerHttpClient { + + private final AtomicReference request; + + CapturingDockerHttpClient(AtomicReference request) { + this.request = request; + } + + @Override + public Response execute(Request request) { + this.request.set(request); + return new EmptyResponse(); + } + + @Override + public void close() throws IOException { + } + } + + private static class EmptyResponse implements DockerHttpClient.Response { + + @Override + public int getStatusCode() { + return 200; + } + + @Override + public Map> getHeaders() { + return Collections.emptyMap(); + } + + @Override + public InputStream getBody() { + return new ByteArrayInputStream(new byte[0]); + } + + @Override + public void close() { + } + } +} diff --git a/docker-java/src/test/java/com/github/dockerjava/junit/PrivateRegistryRule.java b/docker-java/src/test/java/com/github/dockerjava/junit/PrivateRegistryRule.java index 327bfc9415..40dcb7b59d 100644 --- a/docker-java/src/test/java/com/github/dockerjava/junit/PrivateRegistryRule.java +++ b/docker-java/src/test/java/com/github/dockerjava/junit/PrivateRegistryRule.java @@ -12,6 +12,7 @@ import org.junit.rules.ExternalResource; import java.io.File; +import java.util.Objects; import java.util.concurrent.TimeUnit; import static com.github.dockerjava.api.model.HostConfig.newHostConfig; @@ -20,7 +21,7 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; -public class PrivateRegistryRule extends ExternalResource { +public class PrivateRegistryRule extends ExternalResource implements AutoCloseable { private final DockerClient dockerClient; @@ -29,7 +30,11 @@ public class PrivateRegistryRule extends ExternalResource { private String containerId; public PrivateRegistryRule() { - this.dockerClient = CmdIT.createDockerClient(DockerRule.config(null)); + this(CmdIT.createDockerClient(DockerRule.config(null))); + } + + public PrivateRegistryRule(DockerClient dockerClient) { + this.dockerClient = Objects.requireNonNull(dockerClient); } public AuthConfig getAuthConfig() { @@ -59,12 +64,20 @@ public String createTestImage(String tagName) { return imgName + ":" + tagName; } + public void start() throws InterruptedException { + startRegistry(); + } + /** * Starts a local test registry when it is not already started and returns the auth configuration for it * This method is synchronized so that only the first invocation starts the registry */ @Override - protected void before() throws Throwable { + protected void before() throws InterruptedException { + startRegistry(); + } + + private void startRegistry() throws InterruptedException { int port = 5050; @@ -116,4 +129,9 @@ protected void after() { .exec(); } } + + @Override + public void close() { + after(); + } }