DefaultDockerClientUnitTest.java

/*-
 * -\-\-
 * docker-client
 * --
 * Copyright (C) 2016 Spotify AB
 * --
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * -/-/-
 */

package com.spotify.docker.client;

import static com.spotify.docker.FixtureUtil.fixture;
import static com.spotify.hamcrest.jackson.JsonMatchers.jsonArray;
import static com.spotify.hamcrest.jackson.JsonMatchers.jsonObject;
import static com.spotify.hamcrest.jackson.JsonMatchers.jsonText;
import static com.spotify.hamcrest.pojo.IsPojo.pojo;
import static java.util.Collections.singletonList;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.io.BaseEncoding;
import com.google.common.io.Resources;
import com.spotify.docker.client.DockerClient.Signal;
import com.spotify.docker.client.auth.RegistryAuthSupplier;
import com.spotify.docker.client.exceptions.ConflictException;
import com.spotify.docker.client.exceptions.DockerException;
import com.spotify.docker.client.exceptions.NodeNotFoundException;
import com.spotify.docker.client.exceptions.NonSwarmNodeException;
import com.spotify.docker.client.exceptions.NotFoundException;
import com.spotify.docker.client.messages.ContainerConfig;
import com.spotify.docker.client.messages.Distribution;
import com.spotify.docker.client.messages.HostConfig;
import com.spotify.docker.client.messages.HostConfig.Bind;
import com.spotify.docker.client.messages.RegistryAuth;
import com.spotify.docker.client.messages.RegistryConfigs;
import com.spotify.docker.client.messages.ServiceCreateResponse;
import com.spotify.docker.client.messages.Volume;
import com.spotify.docker.client.messages.swarm.Config;
import com.spotify.docker.client.messages.swarm.ConfigBind;
import com.spotify.docker.client.messages.swarm.ConfigCreateResponse;
import com.spotify.docker.client.messages.swarm.ConfigFile;
import com.spotify.docker.client.messages.swarm.ConfigSpec;
import com.spotify.docker.client.messages.swarm.ContainerSpec;
import com.spotify.docker.client.messages.swarm.EngineConfig;
import com.spotify.docker.client.messages.swarm.Node;
import com.spotify.docker.client.messages.swarm.NodeDescription;
import com.spotify.docker.client.messages.swarm.NodeInfo;
import com.spotify.docker.client.messages.swarm.NodeSpec;
import com.spotify.docker.client.messages.swarm.Placement;
import com.spotify.docker.client.messages.swarm.Preference;
import com.spotify.docker.client.messages.swarm.ResourceRequirements;
import com.spotify.docker.client.messages.swarm.Service;
import com.spotify.docker.client.messages.swarm.ServiceSpec;
import com.spotify.docker.client.messages.swarm.Spread;
import com.spotify.docker.client.messages.swarm.SwarmJoin;
import com.spotify.docker.client.messages.swarm.Task;
import com.spotify.docker.client.messages.swarm.TaskSpec;
import com.spotify.docker.client.messages.swarm.Version;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Date;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import okio.Buffer;

import org.glassfish.jersey.client.RequestEntityProcessing;
import org.glassfish.jersey.internal.util.Base64;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

/**
 * Tests DefaultDockerClient against a {@link okhttp3.mockwebserver.MockWebServer} instance, so
 * we can assert what the HTTP requests look like that DefaultDockerClient sends and test how
 * DefaltDockerClient behaves given certain responses from the Docker Remote API.
 * <p>
 * This test may not be a true "unit test", but using a MockWebServer where we can control the HTTP
 * responses sent by the server and capture the HTTP requests sent by the class-under-test is far
 * simpler that attempting to mock the {@link javax.ws.rs.client.Client} instance used by
 * DefaultDockerClient, since the Client has such a rich/fluent interface and many methods/classes
 * that would need to be mocked. Ultimately for testing DefaultDockerClient all we care about is
 * the HTTP requests it sends, rather than what HTTP client library it uses.</p>
 * <p>
 * When adding new functionality to DefaultDockerClient, please consider and prioritize adding unit
 * tests to cover the new functionality in this file rather than integration tests that require a
 * real docker daemon in {@link DefaultDockerClientTest}. While integration tests are valuable,
 * they are more brittle and harder to run than a simple unit test that captures/asserts HTTP
 * requests and responses.</p>
 *
 * @see <a href="https://github.com/square/okhttp/tree/master/mockwebserver">
 * https://github.com/square/okhttp/tree/master/mockwebserver</a>
 */
public class DefaultDockerClientUnitTest {

  private final MockWebServer server = new MockWebServer();

  private DefaultDockerClient.Builder builder;

  @Rule
  public ExpectedException thrown = ExpectedException.none();

  @Before
  public void setup() throws Exception {
    server.start();

    builder = DefaultDockerClient.builder();
    builder.uri(server.url("/").uri());
  }

  @After
  public void tearDown() throws Exception {
    server.shutdown();
  }

  @Test
  public void testHostForUnixSocket() {
    final DefaultDockerClient client = DefaultDockerClient.builder()
        .uri("unix:///var/run/docker.sock").build();
    assertThat(client.getHost(), equalTo("localhost"));
  }

  @Test
  public void testHostForLocalHttps() {
    final DefaultDockerClient client = DefaultDockerClient.builder()
        .uri("https://localhost:2375").build();
    assertThat(client.getHost(), equalTo("localhost"));
  }

  @Test
  public void testHostForFqdnHttps() {
    final DefaultDockerClient client = DefaultDockerClient.builder()
        .uri("https://perdu.com:2375").build();
    assertThat(client.getHost(), equalTo("perdu.com"));
  }

  @Test
  public void testHostForIpHttps() {
    final DefaultDockerClient client = DefaultDockerClient.builder()
        .uri("https://192.168.53.103:2375").build();
    assertThat(client.getHost(), equalTo("192.168.53.103"));
  }

  @Test
  public void testHostWithProxy() {
    try {
      System.setProperty("http.proxyHost", "gmodules.com");
      System.setProperty("http.proxyPort", "80");
      final DefaultDockerClient client = DefaultDockerClient.builder()
              .uri("https://192.168.53.103:2375").build();
      assertThat(client.getClient().getConfiguration()
                      .getProperty("jersey.config.client.proxy.uri"),
              equalTo("http://gmodules.com:80"));
    } finally {
      System.clearProperty("http.proxyHost");
      System.clearProperty("http.proxyPort");
    }
  }

  @Test
  public void testHostWithNonProxyHost() {
    try {
      System.setProperty("http.proxyHost", "gmodules.com");
      System.setProperty("http.proxyPort", "80");
      final String nonProxyHostsPropertyValue = "127.0.0.1|localhost|192.168.*";
      final List<String> nonProxyHostsPropertyValues = Arrays.asList(
              nonProxyHostsPropertyValue, "\"" + nonProxyHostsPropertyValue + "\"");
      for (String value : nonProxyHostsPropertyValues) {
        System.setProperty("http.nonProxyHosts", value);
        final DefaultDockerClient client = DefaultDockerClient.builder()
                .uri("https://192.168.53.103:2375").build();
        assertThat((String) client.getClient().getConfiguration()
                        .getProperty("jersey.config.client.proxy.uri"),
                isEmptyOrNullString());
        final DefaultDockerClient client1 = DefaultDockerClient.builder()
                .uri("https://127.0.0.1:2375").build();
        assertThat((String) client1.getClient().getConfiguration()
                        .getProperty("jersey.config.client.proxy.uri"),
                isEmptyOrNullString());
        final DefaultDockerClient client2 = DefaultDockerClient.builder()
                .uri("https://localhost:2375").build();
        assertThat((String) client2.getClient().getConfiguration()
                        .getProperty("jersey.config.client.proxy.uri"),
                isEmptyOrNullString());
      }
    } finally {
      System.clearProperty("http.proxyHost");
      System.clearProperty("http.proxyPort");
      System.clearProperty("http.nonProxyHosts");
    }
  }

  private RecordedRequest takeRequestImmediately() throws InterruptedException {
    return server.takeRequest(1, TimeUnit.MILLISECONDS);
  }

  @Test
  public void testCustomHeaders() throws Exception {
    builder.header("int", 1);
    builder.header("string", "2");
    builder.header("list", Lists.newArrayList("a", "b", "c"));

    server.enqueue(new MockResponse());

    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);
    dockerClient.info();

    final RecordedRequest recordedRequest = takeRequestImmediately();
    assertThat(recordedRequest.getMethod(), is("GET"));
    assertThat(recordedRequest.getPath(), is("/info"));

    assertThat(recordedRequest.getHeader("int"), is("1"));
    assertThat(recordedRequest.getHeader("string"), is("2"));
    // TODO (mbrown): this seems like incorrect behavior - the client should send 3 headers with
    // name "list", not one header with a value of "[a, b, c]"
    assertThat(recordedRequest.getHeaders().values("list"), contains("[a, b, c]"));
  }

  private static JsonNode toJson(Buffer buffer) throws IOException {
    return ObjectMapperProvider.objectMapper().readTree(buffer.inputStream());
  }

  private static JsonNode toJson(final String string) throws IOException {
    return ObjectMapperProvider.objectMapper().readTree(string);
  }

  private static JsonNode toJson(byte[] bytes) throws IOException {
    return ObjectMapperProvider.objectMapper().readTree(bytes);
  }

  private static JsonNode toJson(Object object) {
    return ObjectMapperProvider.objectMapper().valueToTree(object);
  }

  private static ObjectNode createObjectNode() {
    return ObjectMapperProvider.objectMapper().createObjectNode();
  }
  
  @Test
  @SuppressWarnings("unchecked")
  public void testGroupAdd() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    final HostConfig hostConfig = HostConfig.builder()
        .groupAdd("63", "65")
        .build();

    final ContainerConfig containerConfig = ContainerConfig.builder()
        .hostConfig(hostConfig)
        .build();

    server.enqueue(new MockResponse());

    dockerClient.createContainer(containerConfig);

    final RecordedRequest recordedRequest = takeRequestImmediately();

    final JsonNode groupAdd = toJson(recordedRequest.getBody()).get("HostConfig").get("GroupAdd");
    assertThat(groupAdd.isArray(), is(true));

    assertThat(childrenTextNodes((ArrayNode) groupAdd), containsInAnyOrder("63", "65"));
  }

  @Test
  @SuppressWarnings("unchecked")
  public void testCapAddAndDrop() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    final HostConfig hostConfig = HostConfig.builder()
        .capAdd(ImmutableList.of("foo", "bar"))
        .capAdd(ImmutableList.of("baz", "qux"))
        .build();

    final ContainerConfig containerConfig = ContainerConfig.builder()
        .hostConfig(hostConfig)
        .build();

    server.enqueue(new MockResponse());

    dockerClient.createContainer(containerConfig);

    final RecordedRequest recordedRequest = takeRequestImmediately();

    assertThat(recordedRequest.getMethod(), is("POST"));
    assertThat(recordedRequest.getPath(), is("/containers/create"));

    assertThat(recordedRequest.getHeader("Content-Type"), is("application/json"));

    final JsonNode requestJson = toJson(recordedRequest.getBody());
    assertThat(requestJson, is(jsonObject()
        .where("HostConfig", is(jsonObject()
            .where("CapAdd", is(jsonArray(
                containsInAnyOrder(jsonText("baz"), jsonText("qux")))))))));
  }

  private static Set<String> childrenTextNodes(ArrayNode arrayNode) {
    final Set<String> texts = new HashSet<>();
    for (JsonNode child : arrayNode) {
      Preconditions.checkState(child.isTextual(),
          "ArrayNode must only contain text nodes, but found %s in %s",
          child.getNodeType(),
          arrayNode);
      texts.add(child.textValue());
    }
    return texts;
  }

  @Test
  @SuppressWarnings("deprecated")
  public void buildThrowsIfRegistryAuthandRegistryAuthSupplierAreBothSpecified() {
    thrown.expect(IllegalStateException.class);
    thrown.expectMessage("LOGIC ERROR");

    final RegistryAuthSupplier authSupplier = mock(RegistryAuthSupplier.class);

    //noinspection deprecation
    DefaultDockerClient.builder()
        .registryAuth(RegistryAuth.builder().identityToken("hello").build())
        .registryAuthSupplier(authSupplier)
        .build();
  }

  @Test
  public void testBuildPassesMultipleRegistryConfigs() throws Exception {
    final RegistryConfigs registryConfigs = RegistryConfigs.create(ImmutableMap.of(
        "server1", RegistryAuth.builder()
            .serverAddress("server1")
            .username("u1")
            .password("p1")
            .email("e1")
            .build(),

        "server2", RegistryAuth.builder()
            .serverAddress("server2")
            .username("u2")
            .password("p2")
            .email("e2")
            .build()
    ));

    final RegistryAuthSupplier authSupplier = mock(RegistryAuthSupplier.class);
    when(authSupplier.authForBuild()).thenReturn(registryConfigs);

    final DefaultDockerClient client = builder.registryAuthSupplier(authSupplier)
        .build();

    // build() calls /version to check what format of header to send
    enqueueServerApiVersion("1.20");

    server.enqueue(new MockResponse()
            .setResponseCode(200)
            .addHeader("Content-Type", "application/json")
            .setBody(
                fixture("fixtures/1.22/build.json")
            )
    );

    final Path path = Paths.get(Resources.getResource("dockerDirectory").toURI());

    client.build(path);

    final RecordedRequest versionRequest = takeRequestImmediately();
    assertThat(versionRequest.getMethod(), is("GET"));
    assertThat(versionRequest.getPath(), is("/version"));

    final RecordedRequest buildRequest = takeRequestImmediately();
    assertThat(buildRequest.getMethod(), is("POST"));
    assertThat(buildRequest.getPath(), is("/build"));

    final String registryConfigHeader = buildRequest.getHeader("X-Registry-Config");
    assertThat(registryConfigHeader, is(not(nullValue())));

    // check that the JSON in the header is equivalent to what we mocked out above from
    // the registryAuthSupplier
    final JsonNode headerJsonNode = toJson(BaseEncoding.base64().decode(registryConfigHeader));
    assertThat(headerJsonNode, is(toJson(registryConfigs.configs())));
  }

  @Test
  public void testNanoCpus() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    final HostConfig hostConfig = HostConfig.builder()
        .nanoCpus(2_000_000_000L)
        .build();

    final ContainerConfig containerConfig = ContainerConfig.builder()
        .hostConfig(hostConfig)
        .build();

    server.enqueue(new MockResponse());

    dockerClient.createContainer(containerConfig);

    final RecordedRequest recordedRequest = takeRequestImmediately();

    final JsonNode requestJson = toJson(recordedRequest.getBody());
    final JsonNode nanoCpus = requestJson.get("HostConfig").get("NanoCpus");

    assertThat(hostConfig.nanoCpus(), is(nanoCpus.longValue()));
  }

  @Test
  public void testInspectNode() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    // build() calls /version to check what format of header to send
    enqueueServerApiVersion("1.28");
    enqueueServerApiResponse(200, "fixtures/1.28/nodeInfo.json");

    final NodeInfo nodeInfo = dockerClient.inspectNode("24ifsmvkjbyhk");

    assertThat(nodeInfo, notNullValue());
    assertThat(nodeInfo.id(), is("24ifsmvkjbyhk"));
    assertThat(nodeInfo.status(), notNullValue());
    assertThat(nodeInfo.status().addr(), is("172.17.0.2"));
    assertThat(nodeInfo.managerStatus(), notNullValue());
    assertThat(nodeInfo.managerStatus().addr(), is("172.17.0.2:2377"));
    assertThat(nodeInfo.managerStatus().leader(), is(true));
    assertThat(nodeInfo.managerStatus().reachability(), is("reachable"));
  }

  @Test
  public void testInspectNonLeaderNode() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.27");

    server.enqueue(new MockResponse()
        .setResponseCode(200)
        .addHeader("Content-Type", "application/json")
        .setBody(
            fixture("fixtures/1.27/nodeInfoNonLeader.json")
        )
    );

    NodeInfo nodeInfo = dockerClient.inspectNode("24ifsmvkjbyhk");
    assertThat(nodeInfo, notNullValue());
    assertThat(nodeInfo.id(), is("24ifsmvkjbyhk"));
    assertThat(nodeInfo.status(), notNullValue());
    assertThat(nodeInfo.status().addr(), is("172.17.0.2"));
    assertThat(nodeInfo.managerStatus(), notNullValue());
    assertThat(nodeInfo.managerStatus().addr(), is("172.17.0.2:2377"));
    assertThat(nodeInfo.managerStatus().leader(), nullValue());
    assertThat(nodeInfo.managerStatus().reachability(), is("reachable"));
  }

  @Test
  public void testInspectNodeNonManager() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.27");

    server.enqueue(new MockResponse()
        .setResponseCode(200)
        .addHeader("Content-Type", "application/json")
        .setBody(
            fixture("fixtures/1.27/nodeInfoNonManager.json")
        )
    );

    NodeInfo nodeInfo = dockerClient.inspectNode("24ifsmvkjbyhk");
    assertThat(nodeInfo, notNullValue());
    assertThat(nodeInfo.id(), is("24ifsmvkjbyhk"));
    assertThat(nodeInfo.status(), notNullValue());
    assertThat(nodeInfo.status().addr(), is("172.17.0.2"));
    assertThat(nodeInfo.managerStatus(), nullValue());
  }

  @Test(expected = NodeNotFoundException.class)
  public void testInspectMissingNode() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    // build() calls /version to check what format of header to send
    enqueueServerApiVersion("1.28");
    enqueueServerApiEmptyResponse(404);

    dockerClient.inspectNode("24ifsmvkjbyhk");
  }

  @Test(expected = NonSwarmNodeException.class)
  public void testInspectNonSwarmNode() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    // build() calls /version to check what format of header to send
    enqueueServerApiVersion("1.28");
    enqueueServerApiEmptyResponse(503);

    dockerClient.inspectNode("24ifsmvkjbyhk");
  }

  @Test
  public void testUpdateNode() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.28");
    enqueueServerApiResponse(200, "fixtures/1.28/listNodes.json");

    final List<Node> nodes = dockerClient.listNodes();

    assertThat(nodes.size(), is(1));

    final Node node = nodes.get(0);

    assertThat(node.id(), equalTo("24ifsmvkjbyhk"));
    assertThat(node.version().index(), equalTo(8L));
    assertThat(node.spec().name(), equalTo("my-node"));
    assertThat(node.spec().role(), equalTo("manager"));
    assertThat(node.spec().availability(), equalTo("active"));
    assertThat(node.spec().labels(), hasKey(equalTo("foo")));

    final NodeSpec updatedNodeSpec = NodeSpec.builder(node.spec())
        .addLabel("foobar", "foobar")
        .build();

    enqueueServerApiVersion("1.28");
    enqueueServerApiEmptyResponse(200);

    dockerClient.updateNode(node.id(), node.version().index(), updatedNodeSpec);
  }

  @Test(expected = DockerException.class)
  public void testUpdateNodeWithInvalidVersion() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.28");

    final ObjectNode errorMessage = createObjectNode()
        .put("message", "invalid node version: '7'");

    enqueueServerApiResponse(500, errorMessage);

    final NodeSpec nodeSpec = NodeSpec.builder()
        .addLabel("foo", "baz")
        .name("foobar")
        .availability("active")
        .role("manager")
        .build();

    dockerClient.updateNode("24ifsmvkjbyhk", 7L, nodeSpec);
  }

  @Test(expected = NodeNotFoundException.class)
  public void testUpdateMissingNode() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.28");
    enqueueServerApiError(404, "Error updating node: '24ifsmvkjbyhk'");

    final NodeSpec nodeSpec = NodeSpec.builder()
        .addLabel("foo", "baz")
        .name("foobar")
        .availability("active")
        .role("manager")
        .build();

    dockerClient.updateNode("24ifsmvkjbyhk", 8L, nodeSpec);
  }

  @Test(expected = NonSwarmNodeException.class)
  public void testUpdateNonSwarmNode() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.28");
    enqueueServerApiError(503, "Error updating node: '24ifsmvkjbyhk'");

    final NodeSpec nodeSpec = NodeSpec.builder()
        .name("foobar")
        .addLabel("foo", "baz")
        .availability("active")
        .role("manager")
        .build();

    dockerClient.updateNode("24ifsmvkjbyhk", 8L, nodeSpec);
  }

  @Test
  public void testJoinSwarm() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.24");
    enqueueServerApiEmptyResponse(200);

    SwarmJoin swarmJoin = SwarmJoin.builder()
            .joinToken("token_foo")
            .listenAddr("0.0.0.0:2377")
            .remoteAddrs(singletonList("10.0.0.10:2377"))
            .build();

    dockerClient.joinSwarm(swarmJoin);
  }

  private void enqueueServerApiError(final int statusCode, final String message) {
    final ObjectNode errorMessage = createObjectNode()
        .put("message", message);

    enqueueServerApiResponse(statusCode, errorMessage);
  }

  @Test
  public void testDeleteNode() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.24");
    enqueueServerApiEmptyResponse(200);

    dockerClient.deleteNode("node-1234");
  }

  @Test(expected = NodeNotFoundException.class)
  public void testDeleteNode_NodeNotFound() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.24");
    enqueueServerApiEmptyResponse(404);

    dockerClient.deleteNode("node-1234");
  }

  @Test(expected = NonSwarmNodeException.class)
  public void testDeleteNode_NodeNotPartOfSwarm() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.24");
    enqueueServerApiEmptyResponse(503);

    dockerClient.deleteNode("node-1234");
  }

  @Test
  public void testCreateServiceWithWarnings() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    // build() calls /version to check what format of header to send
    enqueueServerApiVersion("1.25");
    enqueueServerApiResponse(201, "fixtures/1.25/createServiceResponse.json");

    final TaskSpec taskSpec = TaskSpec.builder()
        .containerSpec(ContainerSpec.builder()
            .image("this_image_is_not_found_in_the_registry")
            .build())
        .build();

    final ServiceSpec spec = ServiceSpec.builder()
        .name("test")
        .taskTemplate(taskSpec)
        .build();

    final ServiceCreateResponse response = dockerClient.createService(spec);
    assertThat(response.id(), is(notNullValue()));
    assertThat(response.warnings(), is(hasSize(1)));
    assertThat(response.warnings(),
        contains("unable to pin image this_image_is_not_found_in_the_registry to digest"));
  }

  @Test
  public void testServiceLogs() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.25");

    server.enqueue(new MockResponse()
            .setResponseCode(200)
            .addHeader("Content-Type", "text/plain; charset=utf-8")
            .setBody(
                fixture("fixtures/1.25/serviceLogs.txt")
            )
    );

    final LogStream stream = dockerClient.serviceLogs("serviceId", DockerClient.LogsParam.stderr());
    assertThat(stream.readFully(), is("Log Statement"));
  }

  private void enqueueServerApiEmptyResponse(final int statusCode) {
    server.enqueue(new MockResponse()
        .setResponseCode(statusCode)
        .addHeader("Content-Type", "application/json")
    );
  }

  @Test
  public void testCreateServiceWithPlacementPreference()
      throws IOException, DockerException, InterruptedException {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    final ImmutableList<Preference> prefs = ImmutableList.of(
        Preference.create(
            Spread.create(
                "test"
            )
        )
    );

    final TaskSpec taskSpec = TaskSpec.builder()
        .placement(Placement.create(null, prefs))
        .containerSpec(ContainerSpec.builder()
            .image("this_image_is_found_in_the_registry")
            .build())
        .build();

    final ServiceSpec spec = ServiceSpec.builder()
        .name("test")
        .taskTemplate(taskSpec)
        .build();


    enqueueServerApiVersion("1.30");
    enqueueServerApiResponse(201, "fixtures/1.30/createServiceResponse.json");

    final ServiceCreateResponse response = dockerClient.createService(spec);
    assertThat(response.id(), equalTo("ak7w3gjqoa3kuz8xcpnyy0pvl"));

    enqueueServerApiVersion("1.30");
    enqueueServerApiResponse(200, "fixtures/1.30/inspectCreateResponseWithPlacementPrefs.json");

    final Service service = dockerClient.inspectService("ak7w3gjqoa3kuz8xcpnyy0pvl");
    assertThat(service.spec().taskTemplate().placement(), equalTo(taskSpec.placement()));
  }

  @Test
  public void testCreateServiceWithConfig() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    // build() calls /version to check what format of header to send
    enqueueServerApiVersion("1.30");
    enqueueServerApiResponse(201, "fixtures/1.30/configCreateResponse.json");

    final ConfigSpec configSpec = ConfigSpec
        .builder()
        .data(Base64.encodeAsString("foobar"))
        .name("foo.yaml")
        .build();

    final ConfigCreateResponse configCreateResponse = dockerClient.createConfig(configSpec);
    assertThat(configCreateResponse.id(), equalTo("ktnbjxoalbkvbvedmg1urrz8h"));

    final ConfigBind configBind = ConfigBind.builder()
        .configName(configSpec.name())
        .configId(configCreateResponse.id())
        .file(ConfigFile.builder()
          .gid("1000")
          .uid("1000")
          .mode(600L)
          .name(configSpec.name())
          .build()
        ).build();

    final TaskSpec taskSpec = TaskSpec.builder()
        .containerSpec(ContainerSpec.builder()
            .image("this_image_is_found_in_the_registry")
            .configs(ImmutableList.of(configBind))
            .build())
        .build();

    final ServiceSpec spec = ServiceSpec.builder()
        .name("test")
        .taskTemplate(taskSpec)
        .build();

    enqueueServerApiVersion("1.30");
    enqueueServerApiResponse(201, "fixtures/1.30/createServiceResponse.json");

    final ServiceCreateResponse response = dockerClient.createService(spec);
    assertThat(response.id(), equalTo("ak7w3gjqoa3kuz8xcpnyy0pvl"));
  }

  @Test
  public void testListConfigs() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.30");

    server.enqueue(new MockResponse()
        .setResponseCode(200)
        .addHeader("Content-Type", "application/json")
        .setBody(
            fixture("fixtures/1.30/listConfigs.json")
        )
    );

    final List<Config> configs = dockerClient.listConfigs();
    assertThat(configs.size(), equalTo(1));

    final Config config = configs.get(0);

    assertThat(config, notNullValue());
    assertThat(config.id(), equalTo("ktnbjxoalbkvbvedmg1urrz8h"));
    assertThat(config.version().index(), equalTo(11L));

    final ConfigSpec configSpec = config.configSpec();
    assertThat(configSpec.name(), equalTo("server.conf"));
  }

  @Test
  public void testCreateConfig() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.30");

    server.enqueue(new MockResponse()
        .setResponseCode(201)
        .addHeader("Content-Type", "application/json")
        .setBody(
            fixture("fixtures/1.30/inspectConfig.json")
        )
    );

    final ConfigSpec configSpec = ConfigSpec
        .builder()
        .data(Base64.encodeAsString("foobar"))
        .name("foo.yaml")
        .build();

    final ConfigCreateResponse configCreateResponse = dockerClient.createConfig(configSpec);

    assertThat(configCreateResponse.id(), equalTo("ktnbjxoalbkvbvedmg1urrz8h"));
  }

  @Test(expected = ConflictException.class)
  public void testCreateConfig_ConflictingName() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.30");

    server.enqueue(new MockResponse()
        .setResponseCode(409)
        .addHeader("Content-Type", "application/json")
    );

    final ConfigSpec configSpec = ConfigSpec
        .builder()
        .data(Base64.encodeAsString("foobar"))
        .name("foo.yaml")
        .build();

    dockerClient.createConfig(configSpec);
  }

  @Test(expected = NonSwarmNodeException.class)
  public void testCreateConfig_NonSwarmNode() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.30");

    server.enqueue(new MockResponse()
        .setResponseCode(503)
        .addHeader("Content-Type", "application/json")
    );

    final ConfigSpec configSpec = ConfigSpec
        .builder()
        .data(Base64.encodeAsString("foobar"))
        .name("foo.yaml")
        .build();

    dockerClient.createConfig(configSpec);
  }

  @Test
  public void testInspectConfig() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.30");

    server.enqueue(new MockResponse()
        .setResponseCode(200)
        .addHeader("Content-Type", "application/json")
        .setBody(
            fixture("fixtures/1.30/inspectConfig.json")
        )
    );

    final Config config = dockerClient.inspectConfig("ktnbjxoalbkvbvedmg1urrz8h");

    assertThat(config, notNullValue());
    assertThat(config.id(), equalTo("ktnbjxoalbkvbvedmg1urrz8h"));
    assertThat(config.version().index(), equalTo(11L));

    final ConfigSpec configSpec = config.configSpec();
    assertThat(configSpec.name(), equalTo("app-dev.crt"));
  }

  @Test(expected = NotFoundException.class)
  public void testInspectConfig_NotFound() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.30");

    server.enqueue(new MockResponse()
        .setResponseCode(404)
        .addHeader("Content-Type", "application/json")
    );

    dockerClient.inspectConfig("ktnbjxoalbkvbvedmg1urrz8h");
  }

  @Test(expected = NonSwarmNodeException.class)
  public void testInspectConfig_NonSwarmNode() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.30");

    server.enqueue(new MockResponse()
        .setResponseCode(503)
        .addHeader("Content-Type", "application/json")
    );

    dockerClient.inspectConfig("ktnbjxoalbkvbvedmg1urrz8h");
  }

  @Test
  public void testDeleteConfig() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.30");

    server.enqueue(new MockResponse()
        .setResponseCode(204)
        .addHeader("Content-Type", "application/json")
    );

    dockerClient.deleteConfig("ktnbjxoalbkvbvedmg1urrz8h");
  }

  @Test(expected = NotFoundException.class)
  public void testDeleteConfig_NotFound() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.30");

    server.enqueue(new MockResponse()
        .setResponseCode(404)
        .addHeader("Content-Type", "application/json")
    );

    dockerClient.deleteConfig("ktnbjxoalbkvbvedmg1urrz8h");
  }

  @Test(expected = NonSwarmNodeException.class)
  public void testDeleteConfig_NonSwarmNode() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.30");

    server.enqueue(new MockResponse()
        .setResponseCode(503)
        .addHeader("Content-Type", "application/json")
    );

    dockerClient.deleteConfig("ktnbjxoalbkvbvedmg1urrz8h");
  }

  @Test(expected = NotFoundException.class)
  public void testUpdateConfig_NotFound() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.30");

    server.enqueue(new MockResponse()
        .setResponseCode(404)
        .addHeader("Content-Type", "application/json")
    );

    final ConfigSpec configSpec = ConfigSpec
        .builder()
        .data(Base64.encodeAsString("foobar"))
        .name("foo.yaml")
        .build();

    dockerClient.updateConfig("ktnbjxoalbkvbvedmg1urrz8h", 11L, configSpec);
  }

  @Test(expected = NonSwarmNodeException.class)
  public void testUpdateConfig_NonSwarmNode() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.30");

    server.enqueue(new MockResponse()
        .setResponseCode(503)
        .addHeader("Content-Type", "application/json")
    );

    final ConfigSpec configSpec = ConfigSpec
        .builder()
        .data(Base64.encodeAsString("foobar"))
        .name("foo.yaml")
        .build();

    dockerClient.updateConfig("ktnbjxoalbkvbvedmg1urrz8h", 11L, configSpec);
  }

  @Test
  public void testListNodes() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.28");

    server.enqueue(new MockResponse()
        .setResponseCode(200)
        .addHeader("Content-Type", "application/json")
        .setBody(
            fixture("fixtures/1.28/listNodes.json")
        )
    );

    final List<Node> nodes = dockerClient.listNodes();
    assertThat(nodes.size(), equalTo(1));

    final Node node = nodes.get(0);

    assertThat(node, notNullValue());
    assertThat(node.id(), is("24ifsmvkjbyhk"));
    assertThat(node.version().index(), is(8L));

    final NodeSpec nodeSpec = node.spec();
    assertThat(nodeSpec.name(), is("my-node"));
    assertThat(nodeSpec.role(), is("manager"));
    assertThat(nodeSpec.availability(), is("active"));
    assertThat(nodeSpec.labels().keySet(), contains("foo"));

    final NodeDescription desc = node.description();
    assertThat(desc.hostname(), is("bf3067039e47"));
    assertThat(desc.platform().architecture(), is("x86_64"));
    assertThat(desc.platform().os(), is("linux"));
    assertThat(desc.resources().memoryBytes(), is(8272408576L));
    assertThat(desc.resources().nanoCpus(), is(4000000000L));

    final EngineConfig engine = desc.engine();
    assertThat(engine.engineVersion(), is("17.04.0"));
    assertThat(engine.labels().keySet(), contains("foo"));
    assertThat(engine.plugins().size(), equalTo(4));

    assertThat(node.status(), notNullValue());
    assertThat(node.status().addr(), is("172.17.0.2"));
    assertThat(node.managerStatus(), notNullValue());
    assertThat(node.managerStatus().addr(), is("172.17.0.2:2377"));
    assertThat(node.managerStatus().leader(), is(true));
    assertThat(node.managerStatus().reachability(), is("reachable"));
  }

  @Test(expected = DockerException.class)
  public void testListNodesWithServerError() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.28");

    server.enqueue(new MockResponse()
        .setResponseCode(500)
        .addHeader("Content-Type", "application/json")
    );

    dockerClient.listNodes();
  }
  
  @Test
  public void testBindBuilderSelinuxLabeling() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    final Bind bindNoSelinuxLabel = HostConfig.Bind.builder()
        .from("noselinux")
        .to("noselinux")
        .build();

    final Bind bindSharedSelinuxContent = HostConfig.Bind.builder()
        .from("shared")
        .to("shared")
        .selinuxLabeling(true)
        .build();

    final Bind bindPrivateSelinuxContent = HostConfig.Bind.builder()
        .from("private")
        .to("private")
        .selinuxLabeling(false)
        .build();

    final HostConfig hostConfig = HostConfig.builder()
        .binds(bindNoSelinuxLabel, bindSharedSelinuxContent, bindPrivateSelinuxContent)
        .build();

    final ContainerConfig containerConfig = ContainerConfig.builder()
        .hostConfig(hostConfig)
        .build();

    server.enqueue(new MockResponse());

    dockerClient.createContainer(containerConfig);

    final RecordedRequest recordedRequest = takeRequestImmediately();

    final JsonNode requestJson = toJson(recordedRequest.getBody());

    final JsonNode binds = requestJson.get("HostConfig").get("Binds");

    assertThat(binds.isArray(), is(true));

    Set<String> bindSet = childrenTextNodes((ArrayNode) binds);
    assertThat(bindSet, hasSize(3));

    assertThat(bindSet, hasItem(allOf(containsString("noselinux"),
        not(containsString("z")), not(containsString("Z")))));

    assertThat(bindSet, hasItem(allOf(containsString("shared"), containsString("z"))));
    assertThat(bindSet, hasItem(allOf(containsString("private"), containsString("Z"))));
  }
  
  @Test
  public void testKillContainer() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    server.enqueue(new MockResponse());

    final Signal signal = Signal.SIGHUP;
    dockerClient.killContainer("1234", signal);

    final RecordedRequest recordedRequest = takeRequestImmediately();

    final HttpUrl requestUrl = recordedRequest.getRequestUrl();
    assertThat(requestUrl.queryParameter("signal"), equalTo(signal.toString()));
  }

  @Test
  public void testInspectVolume() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    server.enqueue(new MockResponse()
        .setResponseCode(200)
        .addHeader("Content-Type", "application/json")
        .setBody(
            fixture("fixtures/1.33/inspectVolume.json")
        )
    );

    final Volume volume = dockerClient.inspectVolume("my-volume");

    assertThat(volume.name(), is("tardis"));
    assertThat(volume.driver(), is("custom"));
    assertThat(volume.mountpoint(), is("/var/lib/docker/volumes/tardis"));
    assertThat(volume.status(), is(ImmutableMap.of("hello", "world")));
    assertThat(volume.labels(), is(ImmutableMap.of(
        "com.example.some-label", "some-value",
        "com.example.some-other-label", "some-other-value"
    )));
    assertThat(volume.scope(), is("local"));
    assertThat(volume.options(), is(ImmutableMap.of(
        "foo", "bar",
        "baz", "qux"
    )));
  }
  
  @Test
  public void testBufferedRequestEntityProcessing() throws Exception {
    builder.useRequestEntityProcessing(RequestEntityProcessing.BUFFERED);
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);
    
    final HostConfig hostConfig = HostConfig.builder().build();

    final ContainerConfig containerConfig = ContainerConfig.builder()
        .hostConfig(hostConfig)
        .build();

    server.enqueue(new MockResponse());

    dockerClient.createContainer(containerConfig);

    final RecordedRequest recordedRequest = takeRequestImmediately();

    assertThat(recordedRequest.getHeader("Content-Length"), notNullValue());
    assertThat(recordedRequest.getHeader("Transfer-Encoding"), nullValue());
  }
  
  @Test
  public void testChunkedRequestEntityProcessing() throws Exception {
    builder.useRequestEntityProcessing(RequestEntityProcessing.CHUNKED);
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);
    
    final HostConfig hostConfig = HostConfig.builder().build();

    final ContainerConfig containerConfig = ContainerConfig.builder()
        .hostConfig(hostConfig)
        .build();

    server.enqueue(new MockResponse());

    dockerClient.createContainer(containerConfig);

    final RecordedRequest recordedRequest = takeRequestImmediately();

    assertThat(recordedRequest.getHeader("Content-Length"), nullValue());
    assertThat(recordedRequest.getHeader("Transfer-Encoding"), is("chunked"));
  }

  @Test
  public void testGetDistribution() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    server.enqueue(new MockResponse()
        .setResponseCode(200)
        .addHeader("Content-Type", "application/json")
        .setBody(
          fixture("fixtures/1.30/distribution.json")
        )
    );
    final Distribution distribution = dockerClient.getDistribution("my-image");
    assertThat(distribution, notNullValue());
    assertThat(distribution.platforms().size(), is(1));
    assertThat(distribution.platforms().get(0).architecture(), is("amd64"));
    assertThat(distribution.platforms().get(0).os(), is("linux"));
    assertThat(distribution.platforms().get(0).osVersion(), is(""));
    assertThat(distribution.platforms().get(0).variant(), is(""));
    assertThat(distribution.descriptor().size(), is(3987495L));
    assertThat(distribution.descriptor().digest(), is(
        "sha256:c0537ff6a5218ef531ece93d4984efc99bbf3f7497c0a7726c88e2bb7584dc96"));
    assertThat(distribution.descriptor().mediaType(), is(
        "application/vnd.docker.distribution.manifest.v2+json"
    ));
    assertThat(distribution.platforms().get(0).osFeatures(), is(ImmutableList.of(
        "feature1", "feature2"
    )));
    assertThat(distribution.platforms().get(0).features(), is(ImmutableList.of(
        "feature1", "feature2"
    )));
    assertThat(distribution.descriptor().urls(), is(ImmutableList.of(
        "url1", "url2"
    )));
  }

  @Test
  public void testInspectTask() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.24");
    enqueueServerApiResponse(200, "fixtures/1.24/task.json");

    final Task task = dockerClient.inspectTask("0kzzo1i0y4jz6027t0k7aezc7");

    assertThat(task, is(pojo(Task.class)
        .where("id", is("0kzzo1i0y4jz6027t0k7aezc7"))
        .where("version", is(pojo(Version.class)
            .where("index", is(71L))
        ))
        .where("createdAt", is(Date.from(Instant.parse("2016-06-07T21:07:31.171892745Z"))))
        .where("updatedAt", is(Date.from(Instant.parse("2016-06-07T21:07:31.376370513Z"))))
        .where("spec", is(pojo(TaskSpec.class)
            .where("containerSpec", is(pojo(ContainerSpec.class)
                .where("image", is("redis"))
            ))
            .where("resources", is(pojo(ResourceRequirements.class)
                .where("limits",
                    is(pojo(com.spotify.docker.client.messages.swarm.Resources.class)))
                .where("reservations",
                    is(pojo(com.spotify.docker.client.messages.swarm.Resources.class)))
            ))
        ))
    ));
  }


  @Test
  public void testListTaskWithCriteria() throws Exception {
    final DefaultDockerClient dockerClient = new DefaultDockerClient(builder);

    enqueueServerApiVersion("1.24");
    enqueueServerApiResponse(200, "fixtures/1.24/tasks.json");
    final List<Task> tasks = dockerClient.listTasks();
    // Throw away the first request that gets the docker server version
    takeRequestImmediately();
    final RecordedRequest recordedRequest = takeRequestImmediately();
    assertThat(recordedRequest.getRequestUrl().querySize(), is(0));
    assertThat(tasks, contains(
        pojo(Task.class)
            .where("id", is("0kzzo1i0y4jz6027t0k7aezc7")),
        pojo(Task.class)
            .where("id", is("1yljwbmlr8er2waf8orvqpwms"))
    ));

    enqueueServerApiVersion("1.24");
    enqueueServerApiResponse(200, "fixtures/1.24/tasks.json");
    final String taskId = "task-1";
    dockerClient.listTasks(Task.find().taskId(taskId).build());
    takeRequestImmediately();
    final RecordedRequest recordedRequest2 = takeRequestImmediately();
    final HttpUrl requestUrl2 = recordedRequest2.getRequestUrl();
    assertThat(requestUrl2.querySize(), is(1));
    final JsonNode requestJson2 =
        toJson(recordedRequest2.getRequestUrl().queryParameter("filters"));
    assertThat(requestJson2, is(jsonObject()
        .where("id", is(jsonArray(
            contains(jsonText(taskId)))))));

    enqueueServerApiVersion("1.24");
    enqueueServerApiResponse(200, "fixtures/1.24/tasks.json");
    final String serviceName = "service-1";
    dockerClient.listTasks(Task.find().serviceName(serviceName).build());
    takeRequestImmediately();
    final RecordedRequest recordedRequest3 = takeRequestImmediately();
    final HttpUrl requestUrl3 = recordedRequest3.getRequestUrl();
    assertThat(requestUrl3.querySize(), is(1));
    final JsonNode requestJson3 =
        toJson(recordedRequest3.getRequestUrl().queryParameter("filters"));
    assertThat(requestJson3, is(jsonObject()
        .where("service", is(jsonArray(
            contains(jsonText(serviceName)))))));
  }

  private void enqueueServerApiResponse(final int statusCode, final String fileName)
      throws IOException {
    server.enqueue(new MockResponse()
        .setResponseCode(statusCode)
        .addHeader("Content-Type", "application/json")
        .setBody(
            fixture(fileName)
        )
    );
  }

  private void enqueueServerApiResponse(final int statusCode, final ObjectNode objectResponse) {
    server.enqueue(new MockResponse()
        .setResponseCode(statusCode)
        .addHeader("Content-Type", "application/json")
        .setBody(
            objectResponse.toString()
        )
    );
  }

  private void enqueueServerApiVersion(final String apiVersion) {
    enqueueServerApiResponse(200,
        createObjectNode()
            .put("ApiVersion", apiVersion)
            .put("Arch", "foobar")
            .put("GitCommit", "foobar")
            .put("GoVersion", "foobar")
            .put("KernelVersion", "foobar")
            .put("Os", "foobar")
            .put("Version", "1.20")
    );
  }
}