Skip to content

Commit

Permalink
[docker] Allow versioned docker support
Browse files Browse the repository at this point in the history
  • Loading branch information
shs96c committed Mar 12, 2020
1 parent 7b8ab9e commit 63b9bfb
Show file tree
Hide file tree
Showing 34 changed files with 1,498 additions and 262 deletions.
2 changes: 2 additions & 0 deletions java/client/src/org/openqa/selenium/json/Json.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import java.util.Map;

public class Json {
public static final String JSON_UTF_8 = "application/json; charset=utf-8";

public static final Type LIST_OF_MAPS_TYPE = new TypeToken<List<Map<String, Object>>>() {}.getType();
public static final Type MAP_TYPE = new TypeToken<Map<String, Object>>() {}.getType();
public static final Type OBJECT_TYPE = new TypeToken<Object>() {}.getType();
Expand Down
4 changes: 2 additions & 2 deletions java/client/src/org/openqa/selenium/json/TypeToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@
import java.lang.reflect.Type;


abstract class TypeToken<T> {
public abstract class TypeToken<T> {

private final Type type;

TypeToken() {
public TypeToken() {
// This code is taken from Guava's TypeToken class.
Type superclass = getClass().getGenericSuperclass();
if (!(superclass instanceof ParameterizedType)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import org.openqa.selenium.remote.http.HttpResponse;
import org.openqa.selenium.remote.http.RemoteCall;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
Expand Down Expand Up @@ -58,6 +60,15 @@ private HttpResponse makeCall(HttpRequest request) {
Thread.currentThread().interrupt();
throw new RuntimeException("NettyHttpHandler request interrupted", e);
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof UncheckedIOException) {
throw (UncheckedIOException) cause;
}

if (cause instanceof IOException) {
throw new UncheckedIOException((IOException) cause);
}

throw new RuntimeException("NettyHttpHandler request execution error", e);
}
}
Expand Down
13 changes: 7 additions & 6 deletions java/server/src/org/openqa/selenium/docker/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ load("@rules_jvm_external//:defs.bzl", "artifact")

java_library(
name = "docker",
srcs = glob(["*.java"]),
visibility = [
"//java/server/src/org/openqa/selenium/grid/docker:__pkg__",
"//java/server/test/org/openqa/selenium:__subpackages__",
],
srcs = glob(["**/*.java"]),
deps = [
"//java/client/src/org/openqa/selenium:core",
"//java/client/src/org/openqa/selenium/json",
"//java/client/src/org/openqa/selenium/remote",
"//java/client/src/org/openqa/selenium/remote/http",
artifact("com.google.guava:guava"),
],
visibility = [
"//java/server/src/org/openqa/selenium/grid/docker:__pkg__",
"//java/server/test/org/openqa/selenium/docker:__subpackages__",
]
)
45 changes: 13 additions & 32 deletions java/server/src/org/openqa/selenium/docker/Container.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,19 @@

package org.openqa.selenium.docker;

import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.remote.http.Contents;
import org.openqa.selenium.remote.http.HttpHandler;
import org.openqa.selenium.remote.http.HttpRequest;
import org.openqa.selenium.remote.http.HttpResponse;

import java.time.Duration;
import java.util.Objects;
import java.util.logging.Logger;

import static java.net.HttpURLConnection.HTTP_OK;
import static org.openqa.selenium.remote.http.HttpMethod.DELETE;
import static org.openqa.selenium.remote.http.HttpMethod.POST;

public class Container {

public static final Logger LOG = Logger.getLogger(Container.class.getName());
private final HttpHandler client;
private static final Logger LOG = Logger.getLogger(Container.class.getName());
private final DockerProtocol protocol;
private final ContainerId id;

public Container(HttpHandler client, ContainerId id) {
public Container(DockerProtocol protocol, ContainerId id) {
LOG.info("Created container " + id);
this.client = Objects.requireNonNull(client);
this.protocol = Objects.requireNonNull(protocol);
this.id = Objects.requireNonNull(id);
}

Expand All @@ -49,34 +39,25 @@ public ContainerId getId() {

public void start() {
LOG.info("Starting " + getId());
HttpResponse res = client.execute(new HttpRequest(POST, String.format("/containers/%s/start", id)));
if (!res.isSuccessful()) {
throw new WebDriverException("Unable to start container: " + Contents.string(res));
}
protocol.startContainer(id);
}

public void stop(Duration timeout) {
Objects.requireNonNull(timeout);

LOG.info("Stopping " + getId());

String seconds = String.valueOf(timeout.toMillis() / 1000);
Objects.requireNonNull(timeout, "Timeout to wait for must be set.");

HttpRequest request = new HttpRequest(POST, String.format("/containers/%s/stop", id))
.addQueryParameter("t", seconds);
if (protocol.exists(id)) {
LOG.info("Stopping " + getId());

HttpResponse res = client.execute(request);
if (!res.isSuccessful()) {
throw new WebDriverException("Unable to stop container: " + Contents.string(res));
protocol.stopContainer(id, timeout);
}
}

public void delete() {
LOG.info("Removing " + getId());
// Check to see if the container exists
if (protocol.exists(id)) {
LOG.info("Removing " + getId());

HttpResponse res = client.execute(new HttpRequest(DELETE, "/containers/" + id));
if (res.getStatus() != HTTP_OK) {
LOG.warning("Unable to delete container");
protocol.deleteContainer(id);
}
}
}
10 changes: 10 additions & 0 deletions java/server/src/org/openqa/selenium/docker/ContainerInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Multimap;
import org.openqa.selenium.Beta;

import java.util.Map;
import java.util.Objects;

@Beta
public class ContainerInfo {

private final Image image;
Expand Down Expand Up @@ -56,6 +58,14 @@ public ContainerInfo map(Port containerPort, Port hostPort) {
return new ContainerInfo(image, updatedBindings);
}

@Override
public String toString() {
return "ContainerInfo{" +
"image=" + image +
", portBindings=" + portBindings +
'}';
}

private Map<String, Object> toJson() {
Map<String, Object> hostConfig = ImmutableMap.of(
"PortBindings", portBindings.asMap());
Expand Down
127 changes: 30 additions & 97 deletions java/server/src/org/openqa/selenium/docker/Docker.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,129 +17,62 @@

package org.openqa.selenium.docker;

import com.google.common.reflect.TypeToken;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.json.Json;
import org.openqa.selenium.json.JsonException;
import org.openqa.selenium.json.JsonOutput;
import org.openqa.selenium.remote.http.HttpHandler;
import org.openqa.selenium.remote.http.HttpRequest;
import org.openqa.selenium.remote.http.HttpResponse;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.logging.Logger;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.net.HttpURLConnection.HTTP_OK;
import static org.openqa.selenium.json.Json.MAP_TYPE;
import static org.openqa.selenium.remote.http.Contents.string;
import static org.openqa.selenium.remote.http.Contents.utf8String;
import static org.openqa.selenium.remote.http.HttpMethod.GET;
import static org.openqa.selenium.remote.http.HttpMethod.POST;

public class Docker {

private static final Logger LOG = Logger.getLogger(Docker.class.getName());
private static final Json JSON = new Json();

private final HttpHandler client;
protected final HttpHandler client;
private volatile Optional<DockerProtocol> dockerClient;

public Docker(HttpHandler client) {
Objects.requireNonNull(client, "Docker HTTP client must be set.");

this.client = req -> {
HttpResponse resp = client.execute(req);

if (resp.getStatus() < 200 && resp.getStatus() > 200) {
String value = string(resp);
try {
Object obj = JSON.toType(value, Object.class);
if (obj instanceof Map) {
Map<?, ?> map = (Map<?, ?>) obj;
String message = map.get("message") instanceof String ?
(String) map.get("message") :
value;
throw new RuntimeException(message);
}

throw new RuntimeException(value);
} catch (JsonException e) {
throw new RuntimeException(value);
}
}

return resp;
};
this.client = Objects.requireNonNull(client, "HTTP client to use must be set.");
this.dockerClient = Optional.empty();
}

public Image pull(String name, String tag) {
Objects.requireNonNull(name);
Objects.requireNonNull(tag);

findImage(new ImageNamePredicate(name, tag));

LOG.info(String.format("Pulling %s:%s", name, tag));

HttpRequest request = new HttpRequest(POST, "/images/create")
.addQueryParameter("fromImage", name)
.addQueryParameter("tag", tag);

HttpResponse res = client.execute(request);
if (res.getStatus() != HTTP_OK) {
throw new WebDriverException("Unable to pull container: " + name);
}

LOG.info(String.format("Pull of %s:%s complete", name, tag));

return findImage(new ImageNamePredicate(name, tag))
.orElseThrow(() -> new DockerException(
String.format("Cannot find image matching: %s:%s", name, tag)));
public boolean isSupported() {
return getDocker().isPresent();
}

public List<Image> listImages() {
LOG.fine("Listing images");
HttpResponse response = client.execute(new HttpRequest(GET, "/images/json"));

List<ImageSummary> images =
JSON.toType(string(response), new TypeToken<List<ImageSummary>>() {}.getType());

return images.stream()
.map(Image::new)
.collect(toImmutableList());
public String getVersion() {
return getDocker().map(DockerProtocol::version).orElse("unsupported");
}

public Optional<Image> findImage(Predicate<Image> filter) {
Objects.requireNonNull(filter);
public Image getImage(String name) {
Objects.requireNonNull(name, "Image name to get must be set.");

LOG.fine("Finding image: " + filter);
LOG.info("Obtaining image: " + name);

return listImages().stream()
.filter(filter)
.findFirst();
return getDocker()
.map(protocol -> protocol.getImage(name))
.orElseThrow(() -> new DockerException("Unable to get image " + name));
}

public Container create(ContainerInfo info) {
StringBuilder json = new StringBuilder();

try (JsonOutput output = JSON.newOutput(json)) {
output.setPrettyPrint(false);
output.write(info);
}
Objects.requireNonNull(info, "Container info must be set.");

LOG.info("Creating container: " + json);
LOG.info("Creating image from " + info);

HttpRequest request = new HttpRequest(POST, "/containers/create");
request.setContent(utf8String(json));
return getDocker()
.map(protocol -> protocol.create(info))
.orElseThrow(() -> new DockerException("Unable to create container: " + info));
}

HttpResponse response = client.execute(request);
private Optional<DockerProtocol> getDocker() {
if (dockerClient.isPresent()) {
return dockerClient;
}

Map<String, Object> toRead = JSON.toType(string(response), MAP_TYPE);
synchronized (this) {
if (!dockerClient.isPresent()) {
dockerClient = new VersionCommand(client).getDockerProtocol();
}
}

return new Container(client, new ContainerId((String) toRead.get("Id")));
return dockerClient;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ public class DockerException extends RuntimeException {
public DockerException(String message) {
super(message);
}

public DockerException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,16 @@

package org.openqa.selenium.docker;

import java.util.Objects;
import java.util.function.Predicate;
import java.time.Duration;

public class ImageNamePredicate implements Predicate<Image> {
public interface DockerProtocol {
String version();

private final String name;
private final String tag;
Image getImage(String imageName) throws DockerException;

public ImageNamePredicate(String name, String tag) {
this.name = Objects.requireNonNull(name);
this.tag = Objects.requireNonNull(tag);
}

public ImageNamePredicate(String name) {
Objects.requireNonNull(name);
int index = name.indexOf(":");
if (index == -1) {
this.tag = "latest";
this.name = name;
} else {
this.name = name.substring(0, index);
this.tag = name.substring(index + 1);
}

}

@Override
public boolean test(Image image) {
return image.getTags().contains(name + ":" + tag);
}

@Override
public String toString() {
return "by tag: " + name + ":" + tag;
}
Container create(ContainerInfo info);
void startContainer(ContainerId id) throws DockerException;
void stopContainer(ContainerId id, Duration timeout) throws DockerException;
boolean exists(ContainerId id);
void deleteContainer(ContainerId id) throws DockerException;
}
Loading

0 comments on commit 63b9bfb

Please sign in to comment.