diff options
author | Håkon Hallingstad <hakon@yahoo-inc.com> | 2016-08-31 13:14:13 +0200 |
---|---|---|
committer | Håkon Hallingstad <hakon@yahoo-inc.com> | 2016-09-01 12:48:00 +0200 |
commit | c8d9fb3e150cfdcffd14d96df0040c0c6a616736 (patch) | |
tree | 35ffcc06752ba7d47997f368b4e17214853efd9f /node-admin | |
parent | da7a0474414dbb50733180ac0ac52f4b1f9811b5 (diff) |
Need to figure out what to do with the tests using DockerOperations
Diffstat (limited to 'node-admin')
42 files changed, 416 insertions, 1870 deletions
diff --git a/node-admin/pom.xml b/node-admin/pom.xml index 319aceea23c..2658826616c 100644 --- a/node-admin/pom.xml +++ b/node-admin/pom.xml @@ -52,58 +52,6 @@ <version>${project.version}</version> <scope>provided</scope> </dependency> - <dependency> <!-- Remove once code uses docker-api bundle --> - <groupId>com.github.docker-java</groupId> - <artifactId>docker-java</artifactId> - <version>3.0.2</version> - <exclusions> - <exclusion> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-api</artifactId> - </exclusion> - <exclusion> - <groupId>com.google.guava</groupId> - <artifactId>guava</artifactId> - </exclusion> - <exclusion> - <groupId>com.fasterxml.jackson.jaxrs</groupId> - <artifactId>jackson-jaxrs-json-provider</artifactId> - </exclusion> - <exclusion> - <groupId>com.fasterxml.jackson.core</groupId> - <artifactId>jackson-databind</artifactId> - </exclusion> - <exclusion> - <groupId>org.glassfish.jersey.core</groupId> - <artifactId>jersey-client</artifactId> - </exclusion> - <exclusion> - <groupId>org.glassfish.jersey.core</groupId> - <artifactId>jersey-common</artifactId> - </exclusion> - <exclusion> - <groupId>javax.ws.rs</groupId> - <artifactId>javax.ws.rs-api</artifactId> - </exclusion> - <exclusion> - <groupId>com.fasterxml.jackson.core</groupId> - <artifactId>jackson-core</artifactId> - </exclusion> - <exclusion> - <groupId>com.google.code.findbugs</groupId> - <artifactId>annotations</artifactId> - </exclusion> - <exclusion> - <groupId>org.apache.httpcomponents</groupId> - <artifactId>httpcore</artifactId> - </exclusion> - - <exclusion> - <groupId>org.apache.httpcomponents</groupId> - <artifactId>httpclient</artifactId> - </exclusion> - </exclusions> - </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpcore</artifactId> @@ -171,6 +119,8 @@ <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> + <source>1.8</source> + <target>1.8</target> <compilerArgs> <arg>-Xlint:all</arg> <arg>-Werror</arg> diff --git a/node-admin/src/main/application/services.xml b/node-admin/src/main/application/services.xml index fa7307fc5eb..39ca1e23d9a 100644 --- a/node-admin/src/main/application/services.xml +++ b/node-admin/src/main/application/services.xml @@ -6,7 +6,7 @@ <binding>http://*/rest/*</binding> </handler> <component id="node-admin" class="com.yahoo.vespa.hosted.node.admin.provider.ComponentsProviderImpl" bundle="node-admin"/> - <component id="docker" class="com.yahoo.vespa.hosted.node.admin.docker.DockerImpl" bundle="node-admin"/> + <component id="docker" class="com.yahoo.vespa.hosted.dockerapi.DockerImpl" bundle="node-admin"/> <config name='nodeadmin.docker.docker'> <caCertPath>/host/docker/certs/ca_cert.pem</caCertPath> diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/ContainerNodeSpec.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/ContainerNodeSpec.java index b0c10d8d803..7f7514a44fd 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/ContainerNodeSpec.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/ContainerNodeSpec.java @@ -2,8 +2,8 @@ package com.yahoo.vespa.hosted.node.admin; import com.yahoo.vespa.applicationmodel.HostName; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; -import com.yahoo.vespa.hosted.node.admin.docker.DockerImage; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.DockerImage; import com.yahoo.vespa.hosted.node.admin.noderepository.NodeState; import java.util.Objects; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/Container.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/Container.java deleted file mode 100644 index 65929447b0e..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/Container.java +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.docker; - -import com.yahoo.vespa.applicationmodel.HostName; - -import java.util.Objects; - -/** - * @author stiankri - */ -public class Container { - public final HostName hostname; - public final DockerImage image; - public final ContainerName name; - public final boolean isRunning; - - public Container( - final HostName hostname, - final DockerImage image, - final ContainerName containerName, - final boolean isRunning) { - this.hostname = hostname; - this.image = image; - this.name = containerName; - this.isRunning = isRunning; - } - - @Override - public boolean equals(final Object obj) { - if (!(obj instanceof Container)) { - return false; - } - final Container other = (Container) obj; - return Objects.equals(hostname, other.hostname) - && Objects.equals(image, other.image) - && Objects.equals(name, other.name) - && Objects.equals(isRunning, other.isRunning); - } - - @Override - public int hashCode() { - return Objects.hash(hostname, image, name, isRunning); - } - - @Override - public String toString() { - return "Container {" - + " hostname=" + hostname - + " image=" + image - + " name=" + name - + " isRunning=" + isRunning - + "}"; - } -} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/ContainerName.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/ContainerName.java deleted file mode 100644 index 3784c8fcf9b..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/ContainerName.java +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.docker; - -import java.util.Objects; -import java.util.regex.Pattern; - -/** - * Type-safe value wrapper for docker container names. - * - * @author bakksjo - */ -public class ContainerName { - private static final Pattern LEGAL_CONTAINER_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9-]+$"); - private final String name; - - public ContainerName(final String name) { - this.name = Objects.requireNonNull(name); - if (! LEGAL_CONTAINER_NAME_PATTERN.matcher(name).matches()) { - throw new IllegalArgumentException("Illegal container name: " + name + ". Must match " + - LEGAL_CONTAINER_NAME_PATTERN.toString()); - } - } - - public String asString() { - return name; - } - - @Override - public int hashCode() { - return name.hashCode(); - } - - @Override - public boolean equals(final Object o) { - if (!(o instanceof ContainerName)) { - return false; - } - - final ContainerName other = (ContainerName) o; - - return Objects.equals(name, other.name); - } - - @Override - public String toString() { - return getClass().getSimpleName() + " {" - + " name=" + name - + " }"; - } -} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/Docker.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/Docker.java deleted file mode 100644 index 113784568ce..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/Docker.java +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.docker; - -import com.yahoo.vespa.applicationmodel.HostName; - -import java.net.InetAddress; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.CompletableFuture; - -/** - * @author stiankri - */ -public interface Docker { - - void startContainer(DockerImage dockerImage, HostName hostName, ContainerName containerName, InetAddress nodeInetAddress, double minCpuCores, double minDiskAvailableGb, double minMainMemoryAvailableGb); - - void stopContainer(ContainerName containerName); - - void deleteContainer(ContainerName containerName); - - List<Container> getAllManagedContainers(); - - Optional<Container> getContainer(HostName hostname); - - CompletableFuture<DockerImage> pullImageAsync(DockerImage image); - - boolean imageIsDownloaded(DockerImage image); - - String getVespaVersion(ContainerName containerName); - - void deleteImage(DockerImage dockerImage); - - /** - * Returns the local images that are currently not in use by any container. - */ - Set<DockerImage> getUnusedDockerImages(); - - /** - * TODO: Make this function interruptible, see https://github.com/spotify/docker-client/issues/421 - * - * @param args Program arguments. args[0] must be the program filename. - * @throws RuntimeException (or some subclass thereof) on failure, including docker failure, command failure - */ - ProcessResult executeInContainer(ContainerName containerName, String... args); -} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerImage.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerImage.java deleted file mode 100644 index f7cd6f76d3e..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerImage.java +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.docker; - -import java.util.Objects; - -/** - * Type-safe value wrapper for docker image reference. - * - * @author bakksjo - */ -public class DockerImage { - private final String imageId; - - public DockerImage(final String imageId) { - this.imageId = Objects.requireNonNull(imageId); - } - - public String asString() { - return imageId; - } - - @Override - public int hashCode() { - return imageId.hashCode(); - } - - @Override - public boolean equals(final Object o) { - if (!(o instanceof DockerImage)) { - return false; - } - - final DockerImage other = (DockerImage) o; - - return Objects.equals(imageId, other.imageId); - } - - @Override - public String toString() { - return getClass().getSimpleName() + " {" - + " imageId=" + imageId - + " }"; - } -} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerImpl.java deleted file mode 100644 index 915bd3b71f5..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerImpl.java +++ /dev/null @@ -1,566 +0,0 @@ -// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.docker; - -import com.github.dockerjava.api.DockerClient; -import com.github.dockerjava.api.command.CreateContainerCmd; -import com.github.dockerjava.api.command.CreateContainerResponse; -import com.github.dockerjava.api.command.ExecCreateCmdResponse; -import com.github.dockerjava.api.command.ExecStartCmd; -import com.github.dockerjava.api.command.InspectContainerResponse; -import com.github.dockerjava.api.command.InspectExecResponse; -import com.github.dockerjava.api.exception.DockerClientException; -import com.github.dockerjava.api.exception.DockerException; -import com.github.dockerjava.api.model.Bind; -import com.github.dockerjava.api.model.Image; -import com.github.dockerjava.core.DefaultDockerClientConfig; -import com.github.dockerjava.core.DockerClientImpl; -import com.github.dockerjava.core.RemoteApiVersion; -import com.github.dockerjava.core.command.ExecStartResultCallback; -import com.github.dockerjava.core.command.PullImageResultCallback; -import com.github.dockerjava.jaxrs.JerseyDockerCmdExecFactory; -import com.google.common.base.Joiner; -import com.google.common.io.CharStreams; -import com.google.inject.Inject; -import com.yahoo.nodeadmin.docker.DockerConfig; -import com.yahoo.vespa.applicationmodel.HostName; -import static com.yahoo.vespa.defaults.Defaults.getDefaults; - -import com.yahoo.vespa.hosted.node.admin.nodeagent.DockerOperations; -import com.yahoo.vespa.hosted.node.admin.util.Environment; -import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; -import com.yahoo.vespa.hosted.node.maintenance.Maintainer; - -import javax.annotation.concurrent.GuardedBy; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; - - -/** - * @author stiankri - */ -public class DockerImpl implements Docker { - private static final PrefixLogger NODE_ADMIN_LOGGER = PrefixLogger.getNodeAdminLogger(DockerImpl.class); - - private static final int SECONDS_TO_WAIT_BEFORE_KILLING = 10; - private static final String FRAMEWORK_CONTAINER_PREFIX = "/"; - private static final Pattern VESPA_VERSION_PATTERN = Pattern.compile("^(\\S*)$", Pattern.MULTILINE); - - private static final String LABEL_NAME_MANAGEDBY = "com.yahoo.vespa.managedby"; - private static final String LABEL_VALUE_MANAGEDBY = "node-admin"; - private static final Map<String, String> CONTAINER_LABELS = new HashMap<>(); - - private static final int DOCKER_MAX_PER_ROUTE_CONNECTIONS = 10; - private static final int DOCKER_MAX_TOTAL_CONNECTIONS = 100; - private static final int DOCKER_CONNECT_TIMEOUT_MILLIS = (int) TimeUnit.SECONDS.toMillis(100); - private static final int DOCKER_READ_TIMEOUT_MILLIS = (int) TimeUnit.MINUTES.toMillis(30); - static final String DOCKER_CUSTOM_IP6_NETWORK_NAME = "habla"; - - static { - CONTAINER_LABELS.put(LABEL_NAME_MANAGEDBY, LABEL_VALUE_MANAGEDBY); - } - - private static final List<String> DIRECTORIES_TO_MOUNT = Arrays.asList( - getDefaults().underVespaHome("logs"), - getDefaults().underVespaHome("var/cache"), - getDefaults().underVespaHome("var/crash"), - getDefaults().underVespaHome("var/db/jdisc"), - getDefaults().underVespaHome("var/db/vespa"), - getDefaults().underVespaHome("var/jdisc_container"), - getDefaults().underVespaHome("var/jdisc_core"), - getDefaults().underVespaHome("var/logstash-forwarder"), - getDefaults().underVespaHome("var/maven"), - getDefaults().underVespaHome("var/scoreboards"), - getDefaults().underVespaHome("var/service"), - getDefaults().underVespaHome("var/share"), - getDefaults().underVespaHome("var/spool"), - getDefaults().underVespaHome("var/vespa"), - getDefaults().underVespaHome("var/yca"), - getDefaults().underVespaHome("var/ycore++"), - getDefaults().underVespaHome("var/zookeeper")); - - final DockerClient docker; - - private final Object monitor = new Object(); - - @GuardedBy("monitor") - private final Map<DockerImage, CompletableFuture<DockerImage>> scheduledPulls = new HashMap<>(); - - DockerImpl(final DockerClient dockerClient) { - this.docker = dockerClient; - } - - @Inject - public DockerImpl(final DockerConfig config) { - JerseyDockerCmdExecFactory dockerFactory = new JerseyDockerCmdExecFactory() - .withMaxPerRouteConnections(DOCKER_MAX_PER_ROUTE_CONNECTIONS) - .withMaxTotalConnections(DOCKER_MAX_TOTAL_CONNECTIONS) - .withConnectTimeout(DOCKER_CONNECT_TIMEOUT_MILLIS) - .withReadTimeout(DOCKER_READ_TIMEOUT_MILLIS); - - RemoteApiVersion remoteApiVersion; - try { - remoteApiVersion = RemoteApiVersion.parseConfig(DockerClientImpl.getInstance() - .withDockerCmdExecFactory(dockerFactory).versionCmd().exec().getApiVersion()); - NODE_ADMIN_LOGGER.info("Found version of remote docker API: "+ remoteApiVersion); - // From version 1.24 a field was removed which causes trouble with the current docker java code. - // When this is fixed, we can remove this and do not specify version. - if (remoteApiVersion.isGreaterOrEqual(RemoteApiVersion.VERSION_1_24)) { - remoteApiVersion = RemoteApiVersion.VERSION_1_23; - NODE_ADMIN_LOGGER.info("Found version 1.24 or newer of remote API, using 1.23."); - } - } catch (Exception e) { - NODE_ADMIN_LOGGER.error("Failed when trying to figure out remote API version of docker, using 1.23", e); - remoteApiVersion = RemoteApiVersion.VERSION_1_23; - } - - // DockerClientImpl.getInstance().infoCmd().exec().getServerVersion(); - this.docker = DockerClientImpl.getInstance(new DefaultDockerClientConfig.Builder() - .withDockerHost(config.uri()) - .withApiVersion(remoteApiVersion) - .build()) - .withDockerCmdExecFactory(dockerFactory); - } - - @Override - public CompletableFuture<DockerImage> pullImageAsync(final DockerImage image) { - final CompletableFuture<DockerImage> completionListener; - synchronized (monitor) { - if (scheduledPulls.containsKey(image)) { - return scheduledPulls.get(image); - } - completionListener = new CompletableFuture<>(); - scheduledPulls.put(image, completionListener); - } - docker.pullImageCmd(image.asString()).exec(new ImagePullCallback(image)); - return completionListener; - } - - private CompletableFuture<DockerImage> removeScheduledPoll(final DockerImage image) { - synchronized (monitor) { - return scheduledPulls.remove(image); - } - } - - /** - * Check if a given image is already in the local registry - */ - @Override - public boolean imageIsDownloaded(final DockerImage dockerImage) { - try { - List<Image> images = docker.listImagesCmd().withShowAll(true).exec(); - return images.stream(). - flatMap(image -> Arrays.stream(image.getRepoTags())). - anyMatch(tag -> tag.equals(dockerImage.asString())); - } catch (DockerException e) { - throw new RuntimeException("Failed to list image name: '" + dockerImage + "'", e); - } - } - - @Override - public void startContainer( - final DockerImage dockerImage, - final HostName hostName, - final ContainerName containerName, - final InetAddress nodeInetAddress, - double minCpuCores, - double minDiskAvailableGb, - double minMainMemoryAvailableGb) { - try { - String nodeIpAddress = nodeInetAddress.getHostAddress(); - final boolean isRunningIPv6 = nodeInetAddress instanceof Inet6Address; - final double GIGA = Math.pow(2.0, 30.0); - - NODE_ADMIN_LOGGER.info("--- Starting up " + hostName.s() + " with node IP: " + nodeIpAddress + - ", running IPv6: " + isRunningIPv6); - - CreateContainerCmd containerConfigBuilder = docker.createContainerCmd(dockerImage.asString()) - .withName(containerName.asString()) - .withLabels(CONTAINER_LABELS) - .withEnv(new String[]{"CONFIG_SERVER_ADDRESS=" + Joiner.on(',').join(Environment.getConfigServerHosts())}) - .withHostName(hostName.s()) - .withBinds(applicationStorageToMount(containerName)); - - // TODO: Enforce disk constraints - // TODO: Consider if CPU shares or quoata should be set. For now we are just assuming they are - // nicely controlled by docker. - if (minMainMemoryAvailableGb > 0.00001) { - containerConfigBuilder.withMemory((long) (GIGA * minMainMemoryAvailableGb)); - } - - //If container's IP address is IPv6, use our custom network mode and let docker handle networking. - //If container's IP address is IPv4, set up networking manually as before. In the future docker should - //set up the IPv4 network as well. - if (isRunningIPv6) { - containerConfigBuilder.withNetworkMode(DOCKER_CUSTOM_IP6_NETWORK_NAME).withIpv6Address(nodeIpAddress); - } else { - containerConfigBuilder.withNetworkMode("none"); - } - - CreateContainerResponse response = containerConfigBuilder.exec(); - docker.startContainerCmd(response.getId()).exec(); - - if (! isRunningIPv6) { - setupContainerIPv4Networking(containerName, hostName); - } - } catch (IOException | DockerException e) { - throw new RuntimeException("Failed to start container " + containerName.asString(), e); - } - } - - @Override - public String getVespaVersion(final ContainerName containerName) { - ProcessResult result = executeInContainer(containerName, DockerOperations.GET_VESPA_VERSION_COMMAND); - if (!result.isSuccess()) { - throw new RuntimeException("Container " + containerName.asString() + ": Command " - + Arrays.toString(DockerOperations.GET_VESPA_VERSION_COMMAND) + " failed: " + result); - } - return parseVespaVersion(result.getOutput()) - .orElseThrow(() -> new RuntimeException( - "Container " + containerName.asString() + ": Failed to parse vespa version from " - + result.getOutput())); - } - - // Returns empty if vespa version cannot be parsed. - static Optional<String> parseVespaVersion(final String rawVespaVersion) { - if (rawVespaVersion == null) return Optional.empty(); - - final Matcher matcher = VESPA_VERSION_PATTERN.matcher(rawVespaVersion.trim()); - return matcher.find() ? Optional.of(matcher.group(1)) : Optional.empty(); - } - - @Override - public ProcessResult executeInContainer(ContainerName containerName, String... args) { - assert args.length >= 1; - try { - final ExecCreateCmdResponse response = docker.execCreateCmd(containerName.asString()) - .withCmd(args) - .withAttachStdout(true) - .withAttachStderr(true) - .exec(); - - ByteArrayOutputStream output = new ByteArrayOutputStream(); - ByteArrayOutputStream errors = new ByteArrayOutputStream(); - ExecStartCmd execStartCmd = docker.execStartCmd(response.getId()); - execStartCmd.exec(new ExecStartResultCallback(output, errors)).awaitCompletion(); - - final InspectExecResponse state = docker.inspectExecCmd(execStartCmd.getExecId()).exec(); - assert !state.isRunning(); - Integer exitCode = state.getExitCode(); - assert exitCode != null; - - return new ProcessResult(exitCode, new String(output.toByteArray()), new String(errors.toByteArray())); - } catch (DockerException | InterruptedException e) { - throw new RuntimeException("Container " + containerName.asString() - + " failed to execute " + Arrays.toString(args), e); - } - } - - private void setupContainerIPv4Networking(ContainerName containerName, - HostName hostName) throws UnknownHostException { - PrefixLogger logger = PrefixLogger.getNodeAgentLogger(DockerImpl.class, containerName); - - InspectContainerResponse containerInfo = docker.inspectContainerCmd(containerName.asString()).exec(); - InspectContainerResponse.ContainerState state = containerInfo.getState(); - Integer containerPid = -1; - if (state.getRunning()) { - containerPid = state.getPid(); - if (containerPid == null) { - throw new RuntimeException("PID of running container for host " + hostName + " is null"); - } - } - - InetAddress inetAddress = InetAddress.getByName(hostName.s()); - String ipAddress = inetAddress.getHostAddress(); - - final List<String> command = new LinkedList<>(); - command.add("sudo"); - command.add(getDefaults().underVespaHome("libexec/vespa/node-admin/configure-container-networking.py")); - - Environment.NetworkType networkType = Environment.networkType(); - if (networkType != Environment.NetworkType.normal) { - command.add("--" + networkType); - } - command.add(containerPid.toString()); - command.add(ipAddress); - - for (int retry = 0; retry < 30; ++retry) { - try { - runCommand(command); - logger.info("Done setting up network"); - return; - } catch (Exception e) { - final int sleepSecs = 3; - logger.warning("Failed to configure network with command " + command - + ", will retry in " + sleepSecs + " seconds", e); - try { - Thread.sleep(sleepSecs * 1000); - } catch (InterruptedException e1) { - logger.warning("Sleep interrupted", e1); - } - } - } - } - - private void runCommand(final List<String> command) throws Exception { - ProcessBuilder builder = new ProcessBuilder(command); - builder.redirectErrorStream(true); - Process process = builder.start(); - - String output = CharStreams.toString(new InputStreamReader(process.getInputStream())); - int resultCode = process.waitFor(); - if (resultCode != 0) { - throw new Exception("Command " + Joiner.on(' ').join(command) + " failed: " + output); - } - } - - static List<Bind> applicationStorageToMount(ContainerName containerName) { - return Stream.concat( - Stream.of("/etc/hosts:/etc/hosts"), - DIRECTORIES_TO_MOUNT.stream().map(directory -> - Maintainer.pathInHostFromPathInNode(containerName, directory).toString() + - ":" + directory)) - .map(Bind::parse) - .collect(Collectors.toList()); - } - - @Override - public void stopContainer(final ContainerName containerName) { - Optional<com.github.dockerjava.api.model.Container> dockerContainer = getContainerFromName(containerName, true); - if (dockerContainer.isPresent()) { - try { - docker.stopContainerCmd(dockerContainer.get().getId()).withTimeout(SECONDS_TO_WAIT_BEFORE_KILLING).exec(); - } catch (DockerException e) { - throw new RuntimeException("Failed to stop container", e); - } - } - } - - @Override - public void deleteContainer(ContainerName containerName) { - Optional<com.github.dockerjava.api.model.Container> dockerContainer = getContainerFromName(containerName, true); - if (dockerContainer.isPresent()) { - try { - docker.removeContainerCmd(dockerContainer.get().getId()).exec(); - } catch (DockerException e) { - throw new RuntimeException("Failed to delete container", e); - } - } - } - - @Override - public List<Container> getAllManagedContainers() { - try { - return docker.listContainersCmd().withShowAll(true).exec().stream() - .filter(this::isManaged) - .flatMap(this::asContainer) - .collect(Collectors.toList()); - } catch (DockerException e) { - throw new RuntimeException("Could not retrieve all container", e); - } - } - - @Override - public Optional<Container> getContainer(HostName hostname) { - // TODO Don't rely on getAllManagedContainers - return getAllManagedContainers().stream() - .filter(c -> Objects.equals(hostname, c.hostname)) - .findFirst(); - } - - private Stream<Container> asContainer(com.github.dockerjava.api.model.Container dockerClientContainer) { - try { - final InspectContainerResponse response = docker.inspectContainerCmd(dockerClientContainer.getId()).exec(); - return Stream.of(new Container( - new HostName(response.getConfig().getHostName()), - new DockerImage(dockerClientContainer.getImage()), - new ContainerName(decode(response.getName())), - response.getState().getRunning())); - } catch (DockerException e) { - //TODO: do proper exception handling - throw new RuntimeException("Failed talking to docker daemon", e); - } - } - - - private Optional<com.github.dockerjava.api.model.Container> getContainerFromName( - final ContainerName containerName, final boolean alsoGetStoppedContainers) { - try { - return docker.listContainersCmd().withShowAll(alsoGetStoppedContainers).exec().stream() - .filter(this::isManaged) - .filter(container -> matchName(container, containerName.asString())) - .findFirst(); - } catch (DockerException e) { - throw new RuntimeException("Failed to get container from name", e); - } - } - - - private boolean isManaged(final com.github.dockerjava.api.model.Container container) { - final Map<String, String> labels = container.getLabels(); - if (labels == null) { - return false; - } - return LABEL_VALUE_MANAGEDBY.equals(labels.get(LABEL_NAME_MANAGEDBY)); - } - - private boolean matchName(com.github.dockerjava.api.model.Container container, String targetName) { - return Arrays.stream(container.getNames()).anyMatch(encodedName -> decode(encodedName).equals(targetName)); - } - - private String decode(String encodedContainerName) { - return encodedContainerName.substring(FRAMEWORK_CONTAINER_PREFIX.length()); - } - - @Override - public void deleteImage(final DockerImage dockerImage) { - try { - NODE_ADMIN_LOGGER.info("Deleting docker image " + dockerImage); - docker.removeImageCmd(dockerImage.asString()).exec(); - } catch (DockerException e) { - NODE_ADMIN_LOGGER.warning("Could not delete docker image " + dockerImage, e); - } - } - - @Override - public Set<DockerImage> getUnusedDockerImages() { - // Description of concepts and relationships: - // - a docker image has an id, and refers to its parent image (if any) by image id. - // - a docker image may, in addition to id, have multiple tags, but each tag identifies exactly one image. - // - a docker container refers to its image (exactly one) either by image id or by image tag. - // What this method does to find images considered unused, is build a tree of dependencies - // (e.g. container->tag->image->image) and identify image nodes whose only children (if any) are leaf tags. - // In other words, an image node with no children, or only tag children having no children themselves is unused. - // An image node with an image child is considered used. - // An image node with a container child is considered used. - // An image node with a tag child with a container child is considered used. - try { - final Map<String, DockerObject> objects = new HashMap<>(); - final Map<String, String> dependencies = new HashMap<>(); - - // Populate maps with images (including tags) and their dependencies (parents). - for (Image image : docker.listImagesCmd().withShowAll(true).exec()) { - objects.put(image.getId(), new DockerObject(image.getId(), DockerObjectType.IMAGE)); - if (image.getParentId() != null && !image.getParentId().isEmpty()) { - dependencies.put(image.getId(), image.getParentId()); - } - for (String tag : image.getRepoTags()) { - objects.put(tag, new DockerObject(tag, DockerObjectType.IMAGE_TAG)); - dependencies.put(tag, image.getId()); - } - } - - // Populate maps with containers and their dependency to the image they run on. - for (com.github.dockerjava.api.model.Container container : docker.listContainersCmd().withShowAll(true).exec()) { - objects.put(container.getId(), new DockerObject(container.getId(), DockerObjectType.CONTAINER)); - dependencies.put(container.getId(), container.getImage()); - } - - // Now update every object with its dependencies. - dependencies.forEach((fromId, toId) -> { - Optional.ofNullable(objects.get(toId)) - .ifPresent(obj -> obj.addDependee(objects.get(fromId))); - }); - - // Find images that are not in use (i.e. leafs not used by any containers). - return objects.values().stream() - .filter(dockerObject -> dockerObject.type == DockerObjectType.IMAGE) - .filter(dockerObject -> !dockerObject.isInUse()) - .map(obj -> obj.id) - .map(DockerImage::new) - .collect(Collectors.toSet()); - } catch (DockerException e) { - throw new RuntimeException("Unexpected exception", e); - } - } - - // Helper enum for calculating which images are unused. - private enum DockerObjectType { - IMAGE_TAG, IMAGE, CONTAINER - } - - // Helper class for calculating which images are unused. - private static class DockerObject { - public final String id; - public final DockerObjectType type; - private final List<DockerObject> dependees = new LinkedList<>(); - - public DockerObject(final String id, final DockerObjectType type) { - this.id = id; - this.type = type; - } - - public boolean isInUse() { - if (type == DockerObjectType.CONTAINER) { - return true; - } - - if (dependees.isEmpty()) { - return false; - } - - if (type == DockerObjectType.IMAGE) { - if (dependees.stream().anyMatch(obj -> obj.type == DockerObjectType.IMAGE)) { - return true; - } - } - - return dependees.stream().anyMatch(DockerObject::isInUse); - } - - public void addDependee(final DockerObject dockerObject) { - dependees.add(dockerObject); - } - - @Override - public String toString() { - return "DockerObject {" - + " id=" + id - + " type=" + type.name().toLowerCase() - + " inUse=" + isInUse() - + " dependees=" + dependees.stream().map(obj -> obj.id).collect(Collectors.toList()) - + " }"; - } - } - - private class ImagePullCallback extends PullImageResultCallback { - private final DockerImage dockerImage; - - ImagePullCallback(DockerImage dockerImage) { - this.dockerImage = dockerImage; - } - - @Override - public void onError(Throwable throwable) { - removeScheduledPoll(dockerImage).completeExceptionally(throwable); - } - - - @Override - public void onComplete() { - if (imageIsDownloaded(dockerImage)) { - removeScheduledPoll(dockerImage).complete(dockerImage); - } else { - removeScheduledPoll(dockerImage).completeExceptionally( - new DockerClientException("Could not download image: " + dockerImage)); - } - } - } -}
\ No newline at end of file diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/ProcessResult.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/ProcessResult.java deleted file mode 100644 index 95d71997b14..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/ProcessResult.java +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.docker; - -import java.util.Objects; - -public class ProcessResult { - private final int exitStatus; - private final String output; - private final String errors; - - public ProcessResult(int exitStatus, String output, String errors) { - this.exitStatus = exitStatus; - this.output = output; - this.errors = errors; - } - - public boolean isSuccess() { return exitStatus == 0; } - public int getExitStatus() { return exitStatus; } - - public String getOutput() { return output; } - - public String getErrors() { return errors; } - - @Override - public boolean equals(Object o) { - if (!(o instanceof ProcessResult)) return false; - ProcessResult other = (ProcessResult) o; - return Objects.equals(exitStatus, other.exitStatus) - && Objects.equals(output, other.output) - && Objects.equals(errors, other.errors); - } - - @Override - public int hashCode() { - return Objects.hash(exitStatus, output, errors); - } - - @Override - public String toString() { - return "ProcessResult {" - + " exitStatus=" + exitStatus - + " output=" + output - + " errors=" + errors - + " }"; - } -} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/package-info.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/package-info.java deleted file mode 100644 index 9ee1fe58916..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -@ExportPackage -package com.yahoo.vespa.hosted.node.admin.docker; - -import com.yahoo.osgi.annotation.ExportPackage; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/MaintenanceScheduler.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/MaintenanceScheduler.java index f30fc04699c..ac45e3d66c4 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/MaintenanceScheduler.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/MaintenanceScheduler.java @@ -1,6 +1,6 @@ package com.yahoo.vespa.hosted.node.admin.maintenance; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; import java.io.IOException; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/MaintenanceSchedulerImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/MaintenanceSchedulerImpl.java index 0eb5458a515..6374a23a3ba 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/MaintenanceSchedulerImpl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/MaintenanceSchedulerImpl.java @@ -1,7 +1,7 @@ package com.yahoo.vespa.hosted.node.admin.maintenance; import com.yahoo.io.IOUtils; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; import com.yahoo.vespa.hosted.node.maintenance.DeleteOldAppData; import com.yahoo.vespa.hosted.node.maintenance.Maintainer; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImpl.java index 909608d9edf..684e74beafb 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImpl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImpl.java @@ -4,9 +4,9 @@ package com.yahoo.vespa.hosted.node.admin.nodeadmin; import com.yahoo.collections.Pair; import com.yahoo.vespa.applicationmodel.HostName; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; -import com.yahoo.vespa.hosted.node.admin.docker.Container; -import com.yahoo.vespa.hosted.node.admin.docker.Docker; -import com.yahoo.vespa.hosted.node.admin.docker.DockerImage; +import com.yahoo.vespa.hosted.dockerapi.Container; +import com.yahoo.vespa.hosted.dockerapi.Docker; +import com.yahoo.vespa.hosted.dockerapi.DockerImage; import com.yahoo.vespa.hosted.node.admin.maintenance.MaintenanceScheduler; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgent; import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; @@ -129,6 +129,7 @@ public class NodeAdminImpl implements NodeAdmin { // Delete images that have been eligible for some time. firstTimeEligibleForGC.forEach((dockerImage, timestamp) -> { if (currentTime - timestamp > MIN_AGE_IMAGE_GC_MILLIS) { + logger.info("Deleting Docker image " + dockerImage); docker.deleteImage(dockerImage); } }); diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/DockerOperations.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/DockerOperations.java index 917bd44585a..71f0a7ee588 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/DockerOperations.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/DockerOperations.java @@ -1,54 +1,106 @@ // Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.nodeagent; +import com.google.common.base.Joiner; +import com.google.common.io.CharStreams; import com.yahoo.vespa.applicationmodel.HostName; import com.yahoo.vespa.defaults.Defaults; +import com.yahoo.vespa.hosted.dockerapi.Container; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.Docker; +import com.yahoo.vespa.hosted.dockerapi.DockerImage; +import com.yahoo.vespa.hosted.dockerapi.ProcessResult; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; -import com.yahoo.vespa.hosted.node.admin.docker.Container; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; -import com.yahoo.vespa.hosted.node.admin.docker.Docker; -import com.yahoo.vespa.hosted.node.admin.docker.DockerImage; -import com.yahoo.vespa.hosted.node.admin.docker.ProcessResult; import com.yahoo.vespa.hosted.node.admin.noderepository.NodeState; import com.yahoo.vespa.hosted.node.admin.orchestrator.Orchestrator; import com.yahoo.vespa.hosted.node.admin.orchestrator.OrchestratorException; +import com.yahoo.vespa.hosted.node.admin.util.Environment; import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; +import com.yahoo.vespa.hosted.node.maintenance.Maintainer; +import java.io.InputStreamReader; +import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static com.yahoo.vespa.defaults.Defaults.getDefaults; /** * Class that wraps the Docker class and have some tools related to running programs in docker. * @author dybis */ -public class DockerOperations { +public class DockerOperations implements DockerOperationsInterface { static final String NODE_PROGRAM = Defaults.getDefaults().vespaHome() + "bin/vespa-nodectl"; public static final String[] GET_VESPA_VERSION_COMMAND = new String[]{NODE_PROGRAM, "vespa-version"}; private static final String[] RESUME_NODE_COMMAND = new String[] {NODE_PROGRAM, "resume"}; private static final String[] SUSPEND_NODE_COMMAND = new String[] {NODE_PROGRAM, "suspend"}; + private static final Pattern VESPA_VERSION_PATTERN = Pattern.compile("^(\\S*)$", Pattern.MULTILINE); + + private static final List<String> DIRECTORIES_TO_MOUNT = Arrays.asList( + getDefaults().underVespaHome("logs"), + getDefaults().underVespaHome("var/cache"), + getDefaults().underVespaHome("var/crash"), + getDefaults().underVespaHome("var/db/jdisc"), + getDefaults().underVespaHome("var/db/vespa"), + getDefaults().underVespaHome("var/jdisc_container"), + getDefaults().underVespaHome("var/jdisc_core"), + getDefaults().underVespaHome("var/logstash-forwarder"), + getDefaults().underVespaHome("var/maven"), + getDefaults().underVespaHome("var/scoreboards"), + getDefaults().underVespaHome("var/service"), + getDefaults().underVespaHome("var/share"), + getDefaults().underVespaHome("var/spool"), + getDefaults().underVespaHome("var/vespa"), + getDefaults().underVespaHome("var/yca"), + getDefaults().underVespaHome("var/ycore++"), + getDefaults().underVespaHome("var/zookeeper")); + private final Docker docker; public DockerOperations(Docker docker) { this.docker = docker; } - // Returns null on problems + @Override public String getVespaVersionOrNull(ContainerName containerName) { - try { - return docker.getVespaVersion(containerName); - } catch (RuntimeException e) { - PrefixLogger logger = PrefixLogger.getNodeAgentLogger(DockerOperations.class, containerName); - logger.warning("Ignoring failure", e); + PrefixLogger logger = PrefixLogger.getNodeAgentLogger(DockerOperations.class, containerName); + + ProcessResult result = docker.executeInContainer(containerName, DockerOperations.GET_VESPA_VERSION_COMMAND); + if (!result.isSuccess()) { + logger.warning("Container " + containerName.asString() + ": Command " + + Arrays.toString(DockerOperations.GET_VESPA_VERSION_COMMAND) + " failed: " + result); + return null; + } + Optional<String> vespaVersion = parseVespaVersion(result.getOutput()); + if (vespaVersion.isPresent()) { + return vespaVersion.get(); + } else { + logger.warning("Container " + containerName.asString() + ": Failed to parse vespa version from " + + result.getOutput()); return null; } } + // Returns empty if vespa version cannot be parsed. + static Optional<String> parseVespaVersion(final String rawVespaVersion) { + if (rawVespaVersion == null) return Optional.empty(); + + final Matcher matcher = VESPA_VERSION_PATTERN.matcher(rawVespaVersion.trim()); + return matcher.find() ? Optional.of(matcher.group(1)) : Optional.empty(); + } + // Returns true if started + @Override public boolean startContainerIfNeeded(final ContainerNodeSpec nodeSpec) { final Optional<Container> existingContainer = docker.getContainer(nodeSpec.hostname); if (!existingContainer.isPresent()) { @@ -60,10 +112,12 @@ public class DockerOperations { } // Returns true if scheduling download + @Override public boolean shouldScheduleDownloadOfImage(final DockerImage dockerImage) { return !docker.imageIsDownloaded(dockerImage); } + @Override public boolean removeContainerIfNeeded(ContainerNodeSpec nodeSpec, HostName hostname, Orchestrator orchestrator) throws Exception { Optional<Container> existingContainer = docker.getContainer(hostname); @@ -144,26 +198,111 @@ public class DockerOperations { } } - void startContainer(final ContainerNodeSpec nodeSpec) { + @Override + public void startContainer(final ContainerNodeSpec nodeSpec) { PrefixLogger logger = PrefixLogger.getNodeAgentLogger(DockerOperations.class, nodeSpec.containerName); logger.info("Starting container " + nodeSpec.containerName); - InetAddress nodeInetAddress = null; try { - nodeInetAddress = InetAddress.getByName(nodeSpec.hostname.s()); + InetAddress nodeInetAddress = InetAddress.getByName(nodeSpec.hostname.s()); + String nodeIpAddress = nodeInetAddress.getHostAddress(); + final boolean isRunningIPv6 = nodeInetAddress instanceof Inet6Address; + + String configServers = Environment.getConfigServerHosts().stream().map(HostName::toString).collect(Collectors.joining(",")); + Docker.StartContainerCommand command = docker.createStartContainerCommand( + nodeSpec.wantedDockerImage.get(), + nodeSpec.containerName, + nodeSpec.hostname); + command.withEnvironment("CONFIG_SERVER_ADDRESS", configServers); + + command.withVolume("/etc/hosts", "/etc/hosts"); + for (String pathInNode : DIRECTORIES_TO_MOUNT) { + String pathInHost = Maintainer.pathInHostFromPathInNode(nodeSpec.containerName, pathInNode).toString(); + command = command.withVolume(pathInHost, pathInNode); + } + + // TODO: Enforce disk constraints + // TODO: Consider if CPU shares or quoata should be set. For now we are just assuming they are + // nicely controlled by docker. + if (nodeSpec.minMainMemoryAvailableGb.isPresent()) { + long minMainMemoryAvailableMb = (long) (nodeSpec.minMainMemoryAvailableGb.get() * 1024); + if (minMainMemoryAvailableMb > 0) { + command.withMemoryInMb(minMainMemoryAvailableMb); + } + } + + //If container's IP address is IPv6, use our custom network mode and let docker handle networking. + //If container's IP address is IPv4, set up networking manually as before. In the future docker should + //set up the IPv4 network as well. + if (isRunningIPv6) { + // TODO: What's "habla"? Can we change to anything here, or it seems "none" is reserved. + command.withNetworkMode("habla").withIpv6Address(nodeIpAddress); + } else { + command.withNetworkMode("none"); + } + + logger.info("Starting new container with args: " + command); + command.start(); + + if (!isRunningIPv6) { + setupContainerIPv4Networking(nodeSpec.containerName, nodeSpec.hostname, nodeIpAddress); + } } catch (UnknownHostException e) { - logger.error("Failed to get IP address to " + nodeSpec.hostname); + throw new RuntimeException("Failed to start container " + nodeSpec.containerName.asString(), e); + } + } + + private void setupContainerIPv4Networking(ContainerName containerName, + HostName hostName, + String ipAddress) { + PrefixLogger logger = PrefixLogger.getNodeAgentLogger(DockerOperations.class, containerName); + + Docker.ContainerInfo containerInfo = docker.inspectContainer(containerName); + Optional<Integer> containerPid = containerInfo.getPid(); + if (!containerPid.isPresent()) { + throw new RuntimeException("Container " + containerName + " for host " + + hostName + " isn't running (pid not found)"); + } + + final List<String> command = new LinkedList<>(); + command.add("sudo"); + command.add(getDefaults().underVespaHome("libexec/vespa/node-admin/configure-container-networking.py")); + + Environment.NetworkType networkType = Environment.networkType(); + if (networkType != Environment.NetworkType.normal) { + command.add("--" + networkType); + } + command.add(containerPid.get().toString()); + command.add(ipAddress); + + for (int retry = 0; retry < 30; ++retry) { + try { + runCommand(command); + logger.info("Done setting up network"); + return; + } catch (Exception e) { + final int sleepSecs = 3; + logger.warning("Failed to configure network with command " + command + + ", will retry in " + sleepSecs + " seconds", e); + try { + Thread.sleep(sleepSecs * 1000); + } catch (InterruptedException e1) { + logger.warning("Sleep interrupted", e1); + } + } } + } - // TODO: Properly handle absent min* values - docker.startContainer( - nodeSpec.wantedDockerImage.get(), - nodeSpec.hostname, - nodeSpec.containerName, - nodeInetAddress, - nodeSpec.minCpuCores.get(), - nodeSpec.minDiskAvailableGb.get(), - nodeSpec.minMainMemoryAvailableGb.get()); + private void runCommand(final List<String> command) throws Exception { + ProcessBuilder builder = new ProcessBuilder(command); + builder.redirectErrorStream(true); + Process process = builder.start(); + + String output = CharStreams.toString(new InputStreamReader(process.getInputStream())); + int resultCode = process.waitFor(); + if (resultCode != 0) { + throw new Exception("Command " + Joiner.on(' ').join(command) + " failed: " + output); + } } void scheduleDownloadOfImage(final ContainerNodeSpec nodeSpec, Runnable callback) { @@ -225,6 +364,7 @@ public class DockerOperations { } + @Override public void executeResume(ContainerName containerName) { Optional<ProcessResult> result = executeOptionalProgram(containerName, RESUME_NODE_COMMAND); diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/DockerOperationsInterface.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/DockerOperationsInterface.java new file mode 100644 index 00000000000..8606efc854f --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/DockerOperationsInterface.java @@ -0,0 +1,25 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.node.admin.nodeagent; + +import com.yahoo.vespa.applicationmodel.HostName; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.DockerImage; +import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; +import com.yahoo.vespa.hosted.node.admin.orchestrator.Orchestrator; + +public interface DockerOperationsInterface { + String getVespaVersionOrNull(ContainerName containerName); + + // Returns true if started + boolean startContainerIfNeeded(ContainerNodeSpec nodeSpec); + + // Returns true if scheduling download + boolean shouldScheduleDownloadOfImage(DockerImage dockerImage); + + boolean removeContainerIfNeeded(ContainerNodeSpec nodeSpec, HostName hostname, Orchestrator orchestrator) + throws Exception; + + void startContainer(ContainerNodeSpec nodeSpec); + + void executeResume(ContainerName containerName); +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java index d88831f2922..76aa629c76c 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java @@ -3,7 +3,7 @@ package com.yahoo.vespa.hosted.node.admin.nodeagent; import com.yahoo.vespa.applicationmodel.HostName; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; -import com.yahoo.vespa.hosted.node.admin.docker.DockerImage; +import com.yahoo.vespa.hosted.dockerapi.DockerImage; import com.yahoo.vespa.hosted.node.admin.maintenance.MaintenanceScheduler; import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository; import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepositoryImpl; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAttributes.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAttributes.java index b848b8c8e6a..3656b8b59d1 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAttributes.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAttributes.java @@ -1,7 +1,7 @@ // Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.nodeagent; -import com.yahoo.vespa.hosted.node.admin.docker.DockerImage; +import com.yahoo.vespa.hosted.dockerapi.DockerImage; import java.util.Objects; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepository.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepository.java index 8829c3d4487..164ec9379f7 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepository.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepository.java @@ -3,7 +3,7 @@ package com.yahoo.vespa.hosted.node.admin.noderepository; import com.yahoo.vespa.applicationmodel.HostName; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; -import com.yahoo.vespa.hosted.node.admin.docker.DockerImage; +import com.yahoo.vespa.hosted.dockerapi.DockerImage; import java.io.IOException; import java.util.List; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImpl.java index b46dda2f7a1..e1059e65cdd 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImpl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImpl.java @@ -3,8 +3,8 @@ package com.yahoo.vespa.hosted.node.admin.noderepository; import com.yahoo.vespa.applicationmodel.HostName; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; -import com.yahoo.vespa.hosted.node.admin.docker.DockerImage; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.DockerImage; import com.yahoo.vespa.hosted.node.admin.noderepository.bindings.GetNodesResponse; import com.yahoo.vespa.hosted.node.admin.noderepository.bindings.NodeReadyResponse; import com.yahoo.vespa.hosted.node.admin.noderepository.bindings.UpdateNodeAttributesRequestBody; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/ComponentsProviderImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/ComponentsProviderImpl.java index 3a21c7d4f0b..bf2f12e0884 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/ComponentsProviderImpl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/ComponentsProviderImpl.java @@ -9,7 +9,7 @@ import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminImpl; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdater; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgent; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgentImpl; -import com.yahoo.vespa.hosted.node.admin.docker.Docker; +import com.yahoo.vespa.hosted.dockerapi.Docker; import com.yahoo.vespa.hosted.node.admin.nodeagent.DockerOperations; import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository; import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepositoryImpl; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/PrefixLogger.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/PrefixLogger.java index 42b08b6c96b..12d581c0583 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/PrefixLogger.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/PrefixLogger.java @@ -1,7 +1,7 @@ package com.yahoo.vespa.hosted.node.admin.util; import com.yahoo.log.LogLevel; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; import java.util.logging.Level; import java.util.logging.Logger; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/VespaSSLConfig.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/VespaSSLConfig.java deleted file mode 100644 index d2df5f03c57..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/VespaSSLConfig.java +++ /dev/null @@ -1,230 +0,0 @@ -package com.yahoo.vespa.hosted.node.admin.util; - -import com.github.dockerjava.api.exception.DockerClientException; -import com.github.dockerjava.core.SSLConfig; -import com.yahoo.nodeadmin.docker.DockerConfig; -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; -import org.bouncycastle.cert.X509CertificateHolder; -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.openssl.PEMKeyPair; -import org.bouncycastle.openssl.PEMParser; -import org.glassfish.jersey.SslConfigurator; - -import javax.net.ssl.SSLContext; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.Reader; -import java.io.StringReader; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.security.KeyFactory; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.Security; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.util.ArrayList; -import java.util.List; - -import static java.util.Objects.requireNonNull; - - -/** - * This class is based off {@link com.github.dockerjava.core.LocalDirectorySSLConfig}, but with the ability to - * specify path to each of the certificates instead of directory path. Additionally it includes - * {@link com.github.dockerjava.core.util.CertificateUtils} because of version conflict of with - * com.google.code.findbugs.annotations - */ -public class VespaSSLConfig implements SSLConfig { - private final DockerConfig config; - - public VespaSSLConfig(DockerConfig config) { - this.config = config; - } - - @Override - public SSLContext getSSLContext() { - try { - Security.addProvider(new BouncyCastleProvider()); - - // properties acrobatics not needed for java > 1.6 - String httpProtocols = System.getProperty("https.protocols"); - System.setProperty("https.protocols", "TLSv1"); - SslConfigurator sslConfig = SslConfigurator.newInstance(true); - if (httpProtocols != null) { - System.setProperty("https.protocols", httpProtocols); - } - - String keypem = new String(Files.readAllBytes(Paths.get(config.clientKeyPath()))); - String certpem = new String(Files.readAllBytes(Paths.get(config.clientCertPath()))); - String capem = new String(Files.readAllBytes(Paths.get(config.caCertPath()))); - - sslConfig.keyStore(createKeyStore(keypem, certpem)); - sslConfig.keyStorePassword("docker"); - sslConfig.trustStore(createTrustStore(capem)); - - return sslConfig.createSSLContext(); - } catch (Exception e) { - throw new DockerClientException(e.getMessage(), e); - } - } - - public static KeyStore createKeyStore(final String keypem, final String certpem) throws NoSuchAlgorithmException, - InvalidKeySpecException, IOException, CertificateException, KeyStoreException { - PrivateKey privateKey = loadPrivateKey(keypem); - requireNonNull(privateKey); - List<Certificate> privateCertificates = loadCertificates(certpem); - - KeyStore keyStore = KeyStore.getInstance("JKS"); - keyStore.load(null); - - keyStore.setKeyEntry("docker", - privateKey, - "docker".toCharArray(), - privateCertificates.toArray(new Certificate[privateCertificates.size()]) - ); - - return keyStore; - } - - /** - * from "cert.pem" String - */ - private static List<Certificate> loadCertificates(final String certpem) throws IOException, - CertificateException { - final StringReader certReader = new StringReader(certpem); - try (BufferedReader reader = new BufferedReader(certReader)) { - return loadCertificates(reader); - } - } - - /** - * "cert.pem" from reader - */ - private static List<Certificate> loadCertificates(final Reader reader) throws IOException, - CertificateException { - try (PEMParser pemParser = new PEMParser(reader)) { - List<Certificate> certificates = new ArrayList<>(); - - JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter().setProvider("BC"); - Object certObj = pemParser.readObject(); - - if (certObj instanceof X509CertificateHolder) { - X509CertificateHolder certificateHolder = (X509CertificateHolder) certObj; - certificates.add(certificateConverter.getCertificate(certificateHolder)); - } - - return certificates; - } - } - - - /** - * Return private key ("key.pem") from Reader - */ - private static PrivateKey loadPrivateKey(final Reader reader) throws IOException, NoSuchAlgorithmException, - InvalidKeySpecException { - try (PEMParser pemParser = new PEMParser(reader)) { - Object readObject = pemParser.readObject(); - while (readObject != null) { - if (readObject instanceof PEMKeyPair) { - PEMKeyPair pemKeyPair = (PEMKeyPair) readObject; - PrivateKey privateKey = guessKey(pemKeyPair.getPrivateKeyInfo().getEncoded()); - if (privateKey != null) { - return privateKey; - } - } else if (readObject instanceof PrivateKeyInfo) { - PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) readObject; - PrivateKey privateKey = guessKey(privateKeyInfo.getEncoded()); - if (privateKey != null) { - return privateKey; - } - } else if (readObject instanceof ASN1ObjectIdentifier) { - // no idea how it can be used - final ASN1ObjectIdentifier asn1ObjectIdentifier = (ASN1ObjectIdentifier) readObject; - } - - readObject = pemParser.readObject(); - } - } - - return null; - } - - private static PrivateKey guessKey(byte[] encodedKey) throws NoSuchAlgorithmException { - //no way to know, so iterate - for (String guessFactory : new String[]{"RSA", "ECDSA"}) { - try { - KeyFactory factory = KeyFactory.getInstance(guessFactory); - - PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedKey); - return factory.generatePrivate(privateKeySpec); - } catch (InvalidKeySpecException ignore) { - } - } - - return null; - } - - /** - * Return KeyPair from "key.pem" - */ - private static PrivateKey loadPrivateKey(final String keypem) throws IOException, NoSuchAlgorithmException, - InvalidKeySpecException { - try (StringReader certReader = new StringReader(keypem); - BufferedReader reader = new BufferedReader(certReader)) { - return loadPrivateKey(reader); - } - } - - /** - * "ca.pem" from String - */ - public static KeyStore createTrustStore(String capem) throws IOException, CertificateException, - KeyStoreException, NoSuchAlgorithmException { - try (Reader certReader = new StringReader(capem)) { - return createTrustStore(certReader); - } - } - - /** - * "ca.pem" from Reader - */ - public static KeyStore createTrustStore(final Reader certReader) throws IOException, CertificateException, - KeyStoreException, NoSuchAlgorithmException { - try (PEMParser pemParser = new PEMParser(certReader)) { - X509CertificateHolder certificateHolder = (X509CertificateHolder) pemParser.readObject(); - Certificate caCertificate = new JcaX509CertificateConverter() - .setProvider("BC") - .getCertificate(certificateHolder); - - KeyStore trustStore = KeyStore.getInstance("JKS"); - trustStore.load(null); - trustStore.setCertificateEntry("ca", caCertificate); - - return trustStore; - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - VespaSSLConfig that = (VespaSSLConfig) o; - - return config.equals(that.config); - - } - - @Override - public int hashCode() { - return config.hashCode(); - } -} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/maintenance/Maintainer.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/maintenance/Maintainer.java index 488de969d3c..e57777f37e0 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/maintenance/Maintainer.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/maintenance/Maintainer.java @@ -1,6 +1,6 @@ package com.yahoo.vespa.hosted.node.maintenance; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; import io.airlift.airline.Cli; import io.airlift.airline.Command; import io.airlift.airline.Help; diff --git a/node-admin/src/main/resources/configdefinitions/docker.def b/node-admin/src/main/resources/configdefinitions/docker.def deleted file mode 100644 index c0b2e6131c4..00000000000 --- a/node-admin/src/main/resources/configdefinitions/docker.def +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -namespace=nodeadmin.docker - -caCertPath string -clientCertPath string -clientKeyPath string -uri string default = "tcp://127.0.0.1:2376" diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/ContainerNameTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/ContainerNameTest.java deleted file mode 100644 index bf157779c43..00000000000 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/ContainerNameTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.yahoo.vespa.hosted.node.admin.docker; - -import org.junit.Test; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -/** - * @author valerijf - */ -public class ContainerNameTest { - @Test - public void testAlphanumericalContainerName() { - String name = "container123"; - ContainerName containerName = new ContainerName(name); - assertThat(containerName.asString(), is(name)); - } - - @Test - public void testAlphanumericalWithDashContainerName() { - String name = "container-123"; - ContainerName containerName = new ContainerName(name); - assertThat(containerName.asString(), is(name)); - } - - @Test(expected=IllegalArgumentException.class) - public void testAlphanumericalWithSlashContainerName() { - new ContainerName("container/123"); - } - - @Test(expected=IllegalArgumentException.class) - public void testEmptyContainerName() { - new ContainerName(""); - } - - @Test(expected=NullPointerException.class) - public void testNullContainerName() { - new ContainerName(null); - } -} diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/DockerImplTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/DockerImplTest.java deleted file mode 100644 index fcae057d42d..00000000000 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/DockerImplTest.java +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.docker; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.dockerjava.api.DockerClient; -import com.github.dockerjava.api.command.ExecCreateCmd; -import com.github.dockerjava.api.command.ExecCreateCmdResponse; -import com.github.dockerjava.api.command.ExecStartCmd; -import com.github.dockerjava.api.command.InspectExecCmd; -import com.github.dockerjava.api.command.InspectExecResponse; -import com.github.dockerjava.api.command.ListContainersCmd; -import com.github.dockerjava.api.command.ListImagesCmd; -import com.github.dockerjava.api.model.Bind; -import com.github.dockerjava.api.model.Image; -import com.github.dockerjava.core.command.ExecStartResultCallback; -import com.yahoo.vespa.defaults.Defaults; -import org.junit.Test; -import org.mockito.Matchers; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.*; - -/** - * @author tonytv - */ -public class DockerImplTest { - @Test - public void data_directories_are_mounted_in_from_the_host() { - List<Bind> binds = DockerImpl.applicationStorageToMount(new ContainerName("my-container")); - - String dataDirectory = Defaults.getDefaults().vespaHome() + "logs"; - String directoryOnHost = "/home/docker/container-storage/my-container" + dataDirectory; - assertThat(binds, hasItem(Bind.parse(directoryOnHost + ":" + dataDirectory))); - } - - @Test - public void vespaVersionIsParsed() { - assertThat(DockerImpl.parseVespaVersion("5.119.53"), is(Optional.of("5.119.53"))); - } - - @Test - public void vespaVersionIsParsedWithSpacesAndNewlines() { - assertThat(DockerImpl.parseVespaVersion("5.119.53\n"), is(Optional.of("5.119.53"))); - assertThat(DockerImpl.parseVespaVersion(" 5.119.53 \n"), is(Optional.of("5.119.53"))); - assertThat(DockerImpl.parseVespaVersion("\n 5.119.53 \n"), is(Optional.of("5.119.53"))); - } - - @Test - public void vespaVersionIsParsedWithIrregularVersionScheme() { - assertThat(DockerImpl.parseVespaVersion("7.2"), is(Optional.of("7.2"))); - assertThat(DockerImpl.parseVespaVersion("8.0-beta"), is(Optional.of("8.0-beta"))); - assertThat(DockerImpl.parseVespaVersion("foo"), is(Optional.of("foo"))); - assertThat(DockerImpl.parseVespaVersion("119"), is(Optional.of("119"))); - } - - @Test - public void vespaVersionIsNotParsedFromNull() { - assertThat(DockerImpl.parseVespaVersion(null), is(Optional.empty())); - } - - @Test - public void vespaVersionIsNotParsedFromEmptyString() { - assertThat(DockerImpl.parseVespaVersion(""), is(Optional.empty())); - } - - @Test - public void vespaVersionIsNotParsedFromUnexpectedContent() { - assertThat(DockerImpl.parseVespaVersion("No such command 'vespanodectl'"), is(Optional.empty())); - } - - @Test - public void testExecuteCompletes() throws Exception { - final String containerId = "container-id"; - final String[] command = new String[] {"/bin/ls", "-l"}; - final String execId = "exec-id"; - final int exitCode = 3; - - final DockerClient dockerClient = mock(DockerClient.class); - - final ExecCreateCmdResponse response = mock(ExecCreateCmdResponse.class); - when(response.getId()).thenReturn(execId); - - final ExecCreateCmd execCreateCmd = mock(ExecCreateCmd.class); - when(dockerClient.execCreateCmd(any(String.class))).thenReturn(execCreateCmd); - when(execCreateCmd.withCmd(Matchers.<String>anyVararg())).thenReturn(execCreateCmd); - when(execCreateCmd.withAttachStdout(any(Boolean.class))).thenReturn(execCreateCmd); - when(execCreateCmd.withAttachStderr(any(Boolean.class))).thenReturn(execCreateCmd); - when(execCreateCmd.exec()).thenReturn(response); - - final ExecStartCmd execStartCmd = mock(ExecStartCmd.class); - when(dockerClient.execStartCmd(any(String.class))).thenReturn(execStartCmd); - when(execStartCmd.exec(any(ExecStartResultCallback.class))).thenReturn(mock(ExecStartResultCallback.class)); - - final InspectExecCmd inspectExecCmd = mock(InspectExecCmd.class); - final InspectExecResponse state = mock(InspectExecResponse.class); - when(dockerClient.inspectExecCmd(any(String.class))).thenReturn(inspectExecCmd); - when(inspectExecCmd.exec()).thenReturn(state); - when(state.isRunning()).thenReturn(false); - when(state.getExitCode()).thenReturn(exitCode); - - final Docker docker = new DockerImpl(dockerClient); - final ProcessResult result = docker.executeInContainer(new ContainerName(containerId), command); - assertThat(result.getExitStatus(), is(exitCode)); - } - - @Test - public void noImagesMeansNoUnusedImages() throws Exception { - ImageGcTester - .withExistingImages() - .expectUnusedImages(); - } - - @Test - public void singleImageWithoutContainersIsUnused() throws Exception { - ImageGcTester - .withExistingImages(new ImageBuilder("image-1")) - .expectUnusedImages("image-1"); - } - - @Test - public void singleImageWithContainerIsUsed() throws Exception { - ImageGcTester - .withExistingImages(ImageBuilder.forId("image-1")) - .andExistingContainers(ContainerBuilder.forId("container-1").withImage("image-1")) - .expectUnusedImages(); - } - - @Test - public void onlyLeafImageIsUnused() throws Exception { - ImageGcTester - .withExistingImages( - ImageBuilder.forId("parent-image"), - ImageBuilder.forId("leaf-image").withParentId("parent-image")) - .expectUnusedImages("leaf-image"); - } - - @Test - public void multipleUnusedImagesAreIdentified() throws Exception { - ImageGcTester - .withExistingImages( - ImageBuilder.forId("image-1"), - ImageBuilder.forId("image-2")) - .expectUnusedImages("image-1", "image-2"); - } - - @Test - public void multipleUnusedLeavesAreIdentified() throws Exception { - ImageGcTester - .withExistingImages( - ImageBuilder.forId("parent-image"), - ImageBuilder.forId("image-1").withParentId("parent-image"), - ImageBuilder.forId("image-2").withParentId("parent-image")) - .expectUnusedImages("image-1", "image-2"); - } - - @Test - public void unusedLeafWithUsedSiblingIsIdentified() throws Exception { - ImageGcTester - .withExistingImages( - ImageBuilder.forId("parent-image"), - ImageBuilder.forId("image-1").withParentId("parent-image"), - ImageBuilder.forId("image-2").withParentId("parent-image")) - .andExistingContainers(ContainerBuilder.forId("vespa-node-1").withImage("image-1")) - .expectUnusedImages("image-2"); - } - - @Test - public void containerCanReferToImageByTag() throws Exception { - ImageGcTester - .withExistingImages(ImageBuilder.forId("image-1").withTag("vespa-6")) - .andExistingContainers(ContainerBuilder.forId("vespa-node-1").withImage("vespa-6")) - .expectUnusedImages(); - } - - @Test - public void taggedImageWithNoContainersIsUnused() throws Exception { - ImageGcTester - .withExistingImages(ImageBuilder.forId("image-1").withTag("vespa-6")) - .expectUnusedImages("image-1"); - } - - private static class ImageGcTester { - private final List<Image> existingImages; - private List<com.github.dockerjava.api.model.Container> existingContainers = Collections.emptyList(); - - private ImageGcTester(final List<Image> images) { - this.existingImages = images; - } - - public static ImageGcTester withExistingImages(final ImageBuilder... images) { - final List<Image> existingImages = Arrays.stream(images) - .map(ImageBuilder::toImage) - .collect(Collectors.toList()); - return new ImageGcTester(existingImages); - } - - public ImageGcTester andExistingContainers(final ContainerBuilder... containers) { - this.existingContainers = Arrays.stream(containers) - .map(ContainerBuilder::toContainer) - .collect(Collectors.toList()); - return this; - } - - public void expectUnusedImages(final String... imageIds) throws Exception { - final DockerClient dockerClient = mock(DockerClient.class); - final Docker docker = new DockerImpl(dockerClient); - final ListImagesCmd listImagesCmd = mock(ListImagesCmd.class); - final ListContainersCmd listContainersCmd = mock(ListContainersCmd.class); - - when(dockerClient.listImagesCmd()).thenReturn(listImagesCmd); - when(listImagesCmd.withShowAll(true)).thenReturn(listImagesCmd); - when(listImagesCmd.exec()).thenReturn(existingImages); - - when(dockerClient.listContainersCmd()).thenReturn(listContainersCmd); - when(listContainersCmd.withShowAll(true)).thenReturn(listContainersCmd); - when(listContainersCmd.exec()).thenReturn(existingContainers); - - final Set<DockerImage> expectedUnusedImages = Arrays.stream(imageIds) - .map(DockerImage::new) - .collect(Collectors.toSet()); - assertThat( - docker.getUnusedDockerImages(), - is(expectedUnusedImages)); - - } - } - - /** - * Serializes object to a JSON string using Jackson, then deserializes it to an instance of toClass - * (again using Jackson). This can be used to create Jackson classes with no public constructors. - * @throws IllegalArgumentException if Jackson fails to serialize or deserialize. - */ - private static <T> T createFrom(Class<T> toClass, Object object) throws IllegalArgumentException { - final String serialized; - try { - serialized = new ObjectMapper().writeValueAsString(object); - } catch (JsonProcessingException e) { - throw new IllegalArgumentException("Failed to serialize object " + object + " to " - + toClass + " with Jackson: " + e, e); - } - try { - return new ObjectMapper().readValue(serialized, toClass); - } catch (IOException e) { - throw new IllegalArgumentException("Failed to convert " + serialized + " to " - + toClass + " with Jackson: " + e, e); - } - } - - // Workaround for Image class that can't be instantiated directly in Java (instantiate via Jackson instead). - private static class ImageBuilder { - // Json property names must match exactly the property names in the Image class. - @JsonProperty("Id") - private final String id; - - @JsonProperty("ParentId") - private String parentId; - - @JsonProperty("RepoTags") - private final List<String> repoTags = new LinkedList<>(); - - private ImageBuilder(String id) { this.id = id; } - - public static ImageBuilder forId(String id) { return new ImageBuilder(id); } - public ImageBuilder withParentId(String parentId) { this.parentId = parentId; return this; } - public ImageBuilder withTag(String tag) { this.repoTags.add(tag); return this; } - - public Image toImage() { return createFrom(Image.class, this); } - } - - // Workaround for Container class that can't be instantiated directly in Java (instantiate via Jackson instead). - private static class ContainerBuilder { - // Json property names must match exactly the property names in the Container class. - @JsonProperty("Id") - private final String id; - - @JsonProperty("Image") - private String image; - - private ContainerBuilder(String id) { this.id = id; } - private static ContainerBuilder forId(final String id) { return new ContainerBuilder(id); } - public ContainerBuilder withImage(String image) { this.image = image; return this; } - - public com.github.dockerjava.api.model.Container toContainer() { - return createFrom(com.github.dockerjava.api.model.Container.class, this); - } - } -} diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/DockerTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/DockerTest.java deleted file mode 100644 index bccd9b79070..00000000000 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/DockerTest.java +++ /dev/null @@ -1,175 +0,0 @@ -package com.yahoo.vespa.hosted.node.admin.docker; - -import com.github.dockerjava.api.model.Network; -import com.github.dockerjava.core.command.BuildImageResultCallback; -import com.yahoo.nodeadmin.docker.DockerConfig; -import com.yahoo.vespa.applicationmodel.HostName; -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.net.SocketException; -import java.util.Collections; -import java.util.concurrent.ExecutionException; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -/** - * @author valerijf - */ -public class DockerTest { - /** - * To run these tests: - * 1. Remove Ignore annotations - * 2. Change ownership of docker.sock - * $ sudo chown <your username> /var/run/docker.sock - * 3. (Temporary) Manually create the docker network used by DockerImpl by running: - * $ sudo docker network create --ipv6 --gateway=<your local IPv6 address> --subnet=fe80::1/16 habla - * 4. (Temporary) Manually build docker test image. Inside src/test/resources/simple-ipv6-server run: - * $ sudo docker build -t "simple-ipv6-server:Dockerfile" . - * 5. (Temporary) Comment out setup() and shutdown() - */ - private static final DockerConfig dockerConfig = new DockerConfig(new DockerConfig.Builder() - .caCertPath("") // Temporary setting it to empty as this field is required, in the future - .clientCertPath("") // DockerConfig should be rewritten and probably moved to docker-api module - .clientKeyPath("") - .uri("unix:///var/run/docker.sock")); - - private static final DockerImpl docker = new DockerImpl(dockerConfig); - private static final DockerImage dockerImage = new DockerImage("simple-ipv6-server:Dockerfile"); - - - @Ignore - @Test - public void testDockerImagePull() throws ExecutionException, InterruptedException { - DockerImage dockerImage = new DockerImage("busybox:1.24.0"); - - // Pull the image and wait for the pull to complete - docker.pullImageAsync(dockerImage).get(); - - // Translate the human readable ID to sha256-hash ID that is returned by getUnusedDockerImages() - DockerImage targetImage = new DockerImage(docker.docker.inspectImageCmd(dockerImage.asString()).exec().getId()); - assertTrue("Image: " + dockerImage + " should be unused", docker.getUnusedDockerImages().contains(targetImage)); - - // Remove the image - docker.deleteImage(dockerImage); - assertFalse("Failed to delete " + dockerImage.asString() + " image", docker.imageIsDownloaded(dockerImage)); - } - - @Ignore - @Test - public void testDockerNetworking() throws InterruptedException, ExecutionException, IOException { - HostName hostName1 = new HostName("docker10.test.yahoo.com"); - HostName hostName2 = new HostName("docker11.test.yahoo.com"); - ContainerName containerName1 = new ContainerName("test-container-1"); - ContainerName containerName2 = new ContainerName("test-container-2"); - InetAddress inetAddress1 = Inet6Address.getByName("fe80::10"); - InetAddress inetAddress2 = Inet6Address.getByName("fe80::11"); - - docker.startContainer(dockerImage, hostName1, containerName1, inetAddress1, 0, 0, 0); - docker.startContainer(dockerImage, hostName2, containerName2, inetAddress2, 0, 0, 0); - - try { - testReachabilityFromHost(containerName1, inetAddress1); - testReachabilityFromHost(containerName2, inetAddress2); - - String[] curlFromNodeToNode = new String[]{"curl", "-g", "http://[" + inetAddress2.getHostAddress() + "%eth0]/ping"}; - while (! docker.executeInContainer(containerName1, curlFromNodeToNode).isSuccess()) { - Thread.sleep(20); - } - ProcessResult result = docker.executeInContainer(containerName1, curlFromNodeToNode); - assertTrue("Could not reach " + containerName2.asString() + " from " + containerName1.asString(), - result.getOutput().equals("pong\n")); - } finally { - docker.stopContainer(containerName1); - docker.deleteContainer(containerName1); - - docker.stopContainer(containerName2); - docker.deleteContainer(containerName2); - } - } - - private void testReachabilityFromHost(ContainerName containerName, InetAddress target) throws IOException, InterruptedException { - String[] curlNodeFromHost = {"curl", "-g", "http://[" + target.getHostAddress() + "%" + getInterfaceName() + "]/ping"}; - while (!exec(curlNodeFromHost).equals("pong\n")) { - Thread.sleep(20); - } - assertTrue("Could not reach " + containerName.asString() + " from host", exec(curlNodeFromHost).equals("pong\n")); - } - - - /** - * Returns IPv6 address of on the "docker0" interface that can be reached by the containers - */ - private static String getLocalIPv6Address() throws SocketException { - return Collections.list(NetworkInterface.getNetworkInterfaces()).stream() - .filter(networkInterface -> networkInterface.getDisplayName().equals("docker0")) - .flatMap(i -> Collections.list(i.getInetAddresses()).stream()) - .filter(ip -> ip instanceof Inet6Address && ip.isLinkLocalAddress()) - .findFirst().orElseThrow(RuntimeException::new) - .getHostAddress().split("%")[0]; - } - - /** - * Returns the display name of the bridge used by our custom docker network. This is used for routing in the - * network tests. The bridge is assumed to be the only IPv6 interface starting with "br-" - */ - private static String getInterfaceName() throws SocketException { - return Collections.list(NetworkInterface.getNetworkInterfaces()).stream() - .filter(networkInterface -> networkInterface.getDisplayName().startsWith("br-") && - networkInterface.getInterfaceAddresses().stream() - .anyMatch(ip -> ip.getAddress() instanceof Inet6Address)) - .findFirst().orElseThrow(RuntimeException::new).getDisplayName(); - } - - /** - * Synchronously executes a system process and returns its stdout. Based of {@link com.yahoo.system.ProcessExecuter} - * but could not be reused because of import errors. - */ - private static String exec(String[] command) throws IOException, InterruptedException { - ProcessBuilder pb = new ProcessBuilder(command); - StringBuilder ret = new StringBuilder(); - - Process p = pb.start(); - InputStream is = p.getInputStream(); - while (true) { - int b = is.read(); - if (b==-1) break; - ret.append((char) b); - } - - p.waitFor(); - p.destroy(); - - return ret.toString(); - } - - @Before - public void setup() throws IOException, ExecutionException, InterruptedException { - // Build the image locally - File dockerFilePath = new File("src/test/resources/simple-ipv6-server"); - docker.docker - .buildImageCmd(dockerFilePath) - .withTag(dockerImage.asString()).exec(new BuildImageResultCallback()).awaitCompletion(); - - // Create a temporary network - Network.Ipam ipam = new Network.Ipam().withConfig(new Network.Ipam.Config() - .withSubnet("fe80::1/16").withGateway(getLocalIPv6Address())); - docker.docker.createNetworkCmd().withDriver("bridge").withName(DockerImpl.DOCKER_CUSTOM_IP6_NETWORK_NAME) - .withIpam(ipam).exec(); - } - - @After - public void shutdown() { - // Remove the network we created earlier - docker.docker.removeNetworkCmd(DockerImpl.DOCKER_CUSTOM_IP6_NETWORK_NAME).exec(); - } -} diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/ProcessResultTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/ProcessResultTest.java deleted file mode 100644 index 60ac91025ee..00000000000 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/ProcessResultTest.java +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.docker; - -import org.junit.Test; - -import java.io.IOException; - -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - -public class ProcessResultTest { - @Test - public void testBasicProperties() throws Exception { - ProcessResult processResult = new ProcessResult(0, "foo", "bar"); - assertThat(processResult.getExitStatus(), is(0)); - assertThat(processResult.getOutput(), is("foo")); - assertThat(processResult.isSuccess(), is(true)); - } - - @Test - public void testSuccessFails() throws Exception { - ProcessResult processResult = new ProcessResult(1, "foo", "bar"); - assertThat(processResult.isSuccess(), is(false)); - } -} diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/ComponentsProviderWithMocks.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/ComponentsProviderWithMocks.java index 37018ff0a0d..004265f3346 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/ComponentsProviderWithMocks.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/ComponentsProviderWithMocks.java @@ -7,7 +7,7 @@ import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminImpl; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdater; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgent; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgentImpl; -import com.yahoo.vespa.hosted.node.admin.docker.Docker; +import com.yahoo.vespa.hosted.dockerapi.Docker; import com.yahoo.vespa.hosted.node.admin.nodeagent.DockerOperations; import com.yahoo.vespa.hosted.node.admin.provider.ComponentsProvider; diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerFailTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerFailTest.java index e97f4acda87..3583054bec7 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerFailTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerFailTest.java @@ -3,8 +3,8 @@ package com.yahoo.vespa.hosted.node.admin.integrationTests; import com.yahoo.vespa.applicationmodel.HostName; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; -import com.yahoo.vespa.hosted.node.admin.docker.DockerImage; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.DockerImage; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdmin; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminImpl; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdater; @@ -73,11 +73,10 @@ public class DockerFailTest { Thread.sleep(10); } - while (!DockerMock.getRequests().startsWith("startContainer with DockerImage: DockerImage { imageId=dockerImage }, " + - "HostName: host1, ContainerName: ContainerName { name=container }, InetAddress: null, minCpuCores: 1.0, minDiskAvailableGb: 1.0, " + - "minMainMemoryAvailableGb: 1.0\nexecuteInContainer with ContainerName: ContainerName { name=container }, " + - "args: [/usr/bin/env, test, -x, /opt/vespa/bin/vespa-nodectl]\nexecuteInContainer with ContainerName: " + - "ContainerName { name=container }, args: [/opt/vespa/bin/vespa-nodectl, resume]\n")) { + while (!DockerMock.getRequests().startsWith( + "createStartContainerCommand with DockerImage: DockerImage { imageId=dockerImage }, HostName: hostName, ContainerName: ContainerName { name=container }\n" + + "executeInContainer with ContainerName: ContainerName { name=container }, args: [/usr/bin/env, test, -x, /opt/vespa/bin/vespa-nodectl]\n" + + "executeInContainer with ContainerName: ContainerName { name=container }, args: [/opt/vespa/bin/vespa-nodectl, resume]\n")) { Thread.sleep(10); } } @@ -92,8 +91,7 @@ public class DockerFailTest { public void dockerFailTest() throws InterruptedException { dockerMock.deleteContainer(initialContainerNodeSpec.containerName); - String goal = "startContainer with DockerImage: DockerImage { imageId=dockerImage }, HostName: host1, " + - "ContainerName: ContainerName { name=container }, InetAddress: null, minCpuCores: 1.0, minDiskAvailableGb: 1.0, minMainMemoryAvailableGb: 1.0\n" + + String goal = "createStartContainerCommand with DockerImage: DockerImage { imageId=dockerImage }, HostName: hostName, ContainerName: ContainerName { name=container }\n" + "executeInContainer with ContainerName: ContainerName { name=container }, args: [/usr/bin/env, test, -x, /opt/vespa/bin/vespa-nodectl]\n" + "executeInContainer with ContainerName: ContainerName { name=container }, args: [/opt/vespa/bin/vespa-nodectl, resume]\n" + "deleteContainer with ContainerName: ContainerName { name=container }\n" + diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerMock.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerMock.java index 29af705d44a..aa1c53fd59c 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerMock.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerMock.java @@ -2,13 +2,12 @@ package com.yahoo.vespa.hosted.node.admin.integrationTests; import com.yahoo.vespa.applicationmodel.HostName; -import com.yahoo.vespa.hosted.node.admin.docker.Container; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; -import com.yahoo.vespa.hosted.node.admin.docker.Docker; -import com.yahoo.vespa.hosted.node.admin.docker.DockerImage; -import com.yahoo.vespa.hosted.node.admin.docker.ProcessResult; +import com.yahoo.vespa.hosted.dockerapi.Container; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.Docker; +import com.yahoo.vespa.hosted.dockerapi.DockerImage; +import com.yahoo.vespa.hosted.dockerapi.ProcessResult; -import java.net.InetAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -42,15 +41,28 @@ public class DockerMock implements Docker { } @Override - public void startContainer(DockerImage dockerImage, HostName hostName, ContainerName containerName, InetAddress inetAddress, - double minCpuCores, double minDiskAvailableGb, double minMainMemoryAvailableGb) { + public StartContainerCommand createStartContainerCommand( + DockerImage dockerImage, + ContainerName containerName, + HostName hostName) { synchronized (monitor) { - requests.append("startContainer with DockerImage: ").append(dockerImage).append(", HostName: ").append(hostName) - .append(", ContainerName: ").append(containerName).append(", InetAddress: ").append(inetAddress) - .append(", minCpuCores: ").append(minCpuCores).append(", minDiskAvailableGb: ").append(minDiskAvailableGb) - .append(", minMainMemoryAvailableGb: ").append(minMainMemoryAvailableGb).append("\n"); + requests.append("createStartContainerCommand with DockerImage: ") + .append(dockerImage).append(", HostName: ").append(hostName) + .append(", ContainerName: ").append(containerName).append("\n"); containers.add(new Container(hostName, dockerImage, containerName, true)); } + + return new StartContainerCommandMock(); + } + + @Override + public ContainerInfo inspectContainer(ContainerName containerName) { + return new ContainerInfo() { + @Override + public Optional<Integer> getPid() { + return Optional.of(2); + } + }; } @Override @@ -102,11 +114,6 @@ public class DockerMock implements Docker { } @Override - public String getVespaVersion(ContainerName containerName) { - return null; - } - - @Override public void deleteImage(DockerImage dockerImage) { } @@ -136,5 +143,41 @@ public class DockerMock implements Docker { requests = new StringBuilder(); } } -} + public static class StartContainerCommandMock implements StartContainerCommand { + @Override + public StartContainerCommand withLabel(String name, String value) { + return this; + } + + @Override + public StartContainerCommand withEnvironment(String name, String value) { + return this; + } + + @Override + public StartContainerCommand withVolume(String path, String volumePath) { + return this; + } + + @Override + public StartContainerCommand withMemoryInMb(long megaBytes) { + return this; + } + + @Override + public StartContainerCommand withNetworkMode(String mode) { + return this; + } + + @Override + public StartContainerCommand withIpv6Address(String address) { + return this; + } + + @Override + public void start() { + + } + } +} diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/MaintenanceSchedulerMock.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/MaintenanceSchedulerMock.java index a6c4ed63439..c634cd2aa05 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/MaintenanceSchedulerMock.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/MaintenanceSchedulerMock.java @@ -1,6 +1,6 @@ package com.yahoo.vespa.hosted.node.admin.integrationTests; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; import com.yahoo.vespa.hosted.node.admin.maintenance.MaintenanceScheduler; import java.io.IOException; diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/MultiDockerTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/MultiDockerTest.java index da658e462a9..1f776a4b911 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/MultiDockerTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/MultiDockerTest.java @@ -4,8 +4,8 @@ package com.yahoo.vespa.hosted.node.admin.integrationTests; import com.yahoo.vespa.applicationmodel.HostName; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; -import com.yahoo.vespa.hosted.node.admin.docker.DockerImage; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.DockerImage; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdmin; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminImpl; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdater; diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java index ecdcf5e21e6..0e493cf9219 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java @@ -3,8 +3,8 @@ package com.yahoo.vespa.hosted.node.admin.integrationTests; import com.yahoo.vespa.applicationmodel.HostName; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; -import com.yahoo.vespa.hosted.node.admin.docker.DockerImage; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.DockerImage; import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository; import com.yahoo.vespa.hosted.node.admin.noderepository.NodeState; diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeStateTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeStateTest.java index 7dc0a449913..af1b447b1d2 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeStateTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeStateTest.java @@ -3,8 +3,8 @@ package com.yahoo.vespa.hosted.node.admin.integrationTests; import com.yahoo.vespa.applicationmodel.HostName; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; -import com.yahoo.vespa.hosted.node.admin.docker.DockerImage; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.DockerImage; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdmin; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminImpl; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdater; diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/ResumeTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/ResumeTest.java index 229f10bfe64..c7489fc28aa 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/ResumeTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/ResumeTest.java @@ -8,8 +8,8 @@ import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminImpl; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdater; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgent; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgentImpl; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; -import com.yahoo.vespa.hosted.node.admin.docker.DockerImage; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.DockerImage; import com.yahoo.vespa.hosted.node.admin.nodeagent.DockerOperations; import com.yahoo.vespa.hosted.node.admin.noderepository.NodeState; import org.junit.After; diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImplTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImplTest.java index 312db45c790..2de470f403c 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImplTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImplTest.java @@ -4,10 +4,10 @@ package com.yahoo.vespa.hosted.node.admin.nodeadmin; import com.yahoo.collections.Pair; import com.yahoo.vespa.applicationmodel.HostName; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; -import com.yahoo.vespa.hosted.node.admin.docker.Container; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; -import com.yahoo.vespa.hosted.node.admin.docker.Docker; -import com.yahoo.vespa.hosted.node.admin.docker.DockerImage; +import com.yahoo.vespa.hosted.dockerapi.Container; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.Docker; +import com.yahoo.vespa.hosted.dockerapi.DockerImage; import com.yahoo.vespa.hosted.node.admin.maintenance.MaintenanceScheduler; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgent; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgentImpl; diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdaterTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdaterTest.java index 9c9cdf7b5f7..5b3ce94614c 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdaterTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdaterTest.java @@ -3,7 +3,7 @@ package com.yahoo.vespa.hosted.node.admin.nodeadmin; import com.yahoo.prelude.semantics.RuleBaseException; import com.yahoo.vespa.applicationmodel.HostName; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; import com.yahoo.vespa.hosted.node.admin.integrationTests.OrchestratorMock; import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository; import com.yahoo.vespa.hosted.node.admin.noderepository.NodeState; diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeagent/DockerOperationsTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeagent/DockerOperationsTest.java index c9cd8963617..8f3850dc569 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeagent/DockerOperationsTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeagent/DockerOperationsTest.java @@ -1,20 +1,25 @@ // Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.nodeagent; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; -import com.yahoo.vespa.hosted.node.admin.docker.Docker; -import com.yahoo.vespa.hosted.node.admin.docker.ProcessResult; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.Docker; +import com.yahoo.vespa.hosted.dockerapi.ProcessResult; +import org.hamcrest.CoreMatchers; import org.junit.Test; import org.mockito.InOrder; import java.util.Optional; import static org.hamcrest.core.Is.is; -import static org.junit.Assert.*; +import static org.junit.Assert.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyVararg; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class DockerOperationsTest { private final Docker docker = mock(Docker.class); @@ -80,4 +85,39 @@ public class DockerOperationsTest { assertThat(result.isPresent(), is(true)); assertThat(result.get(), is(actualResult)); } + + @Test + public void vespaVersionIsParsed() { + assertThat(DockerOperations.parseVespaVersion("5.119.53"), CoreMatchers.is(Optional.of("5.119.53"))); + } + + @Test + public void vespaVersionIsParsedWithSpacesAndNewlines() { + assertThat(DockerOperations.parseVespaVersion("5.119.53\n"), CoreMatchers.is(Optional.of("5.119.53"))); + assertThat(DockerOperations.parseVespaVersion(" 5.119.53 \n"), CoreMatchers.is(Optional.of("5.119.53"))); + assertThat(DockerOperations.parseVespaVersion("\n 5.119.53 \n"), CoreMatchers.is(Optional.of("5.119.53"))); + } + + @Test + public void vespaVersionIsParsedWithIrregularVersionScheme() { + assertThat(DockerOperations.parseVespaVersion("7.2"), CoreMatchers.is(Optional.of("7.2"))); + assertThat(DockerOperations.parseVespaVersion("8.0-beta"), CoreMatchers.is(Optional.of("8.0-beta"))); + assertThat(DockerOperations.parseVespaVersion("foo"), CoreMatchers.is(Optional.of("foo"))); + assertThat(DockerOperations.parseVespaVersion("119"), CoreMatchers.is(Optional.of("119"))); + } + + @Test + public void vespaVersionIsNotParsedFromNull() { + assertThat(DockerOperations.parseVespaVersion(null), CoreMatchers.is(Optional.empty())); + } + + @Test + public void vespaVersionIsNotParsedFromEmptyString() { + assertThat(DockerOperations.parseVespaVersion(""), CoreMatchers.is(Optional.empty())); + } + + @Test + public void vespaVersionIsNotParsedFromUnexpectedContent() { + assertThat(DockerOperations.parseVespaVersion("No such command 'vespanodectl'"), CoreMatchers.is(Optional.empty())); + } }
\ No newline at end of file diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImplTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImplTest.java index c50d25d5b67..d2d71c7b0be 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImplTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImplTest.java @@ -2,12 +2,13 @@ package com.yahoo.vespa.hosted.node.admin.nodeagent; import com.yahoo.vespa.applicationmodel.HostName; +import com.yahoo.vespa.hosted.dockerapi.Container; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.Docker; +import com.yahoo.vespa.hosted.dockerapi.DockerImage; +import com.yahoo.vespa.hosted.dockerapi.ProcessResult; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; -import com.yahoo.vespa.hosted.node.admin.docker.Container; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; -import com.yahoo.vespa.hosted.node.admin.docker.Docker; -import com.yahoo.vespa.hosted.node.admin.docker.DockerImage; -import com.yahoo.vespa.hosted.node.admin.docker.ProcessResult; +import com.yahoo.vespa.hosted.node.admin.integrationTests.DockerMock; import com.yahoo.vespa.hosted.node.admin.maintenance.MaintenanceScheduler; import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository; import com.yahoo.vespa.hosted.node.admin.noderepository.NodeState; @@ -22,7 +23,6 @@ import java.util.concurrent.CompletableFuture; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyDouble; import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyVararg; @@ -76,7 +76,8 @@ public class NodeAgentImplTest { when(docker.imageIsDownloaded(dockerImage)).thenReturn(true); when(docker.executeInContainer(eq(containerName), anyVararg())).thenReturn(NODE_PROGRAM_DOESNT_EXIST); - when(docker.getVespaVersion(containerName)).thenReturn(vespaVersion); + when(docker.executeInContainer(eq(containerName), eq(DockerOperations.GET_VESPA_VERSION_COMMAND))) + .thenReturn(new ProcessResult(0, vespaVersion, "")); when(nodeRepository.getContainerNodeSpec(hostName)).thenReturn(Optional.of(nodeSpec)); when(docker.getContainer(hostName)).thenReturn(Optional.of(existingContainer)); @@ -85,14 +86,10 @@ public class NodeAgentImplTest { verify(orchestrator, never()).suspend(any(HostName.class)); verify(docker, never()).stopContainer(any(ContainerName.class)); verify(docker, never()).deleteContainer(any(ContainerName.class)); - verify(docker, never()).startContainer( + verify(docker, never()).createStartContainerCommand( any(DockerImage.class), - any(HostName.class), any(ContainerName.class), - any(InetAddress.class), - anyDouble(), - anyDouble(), - anyDouble()); + any(HostName.class)); verify(docker, times(1)).executeInContainer(any(), anyVararg()); final InOrder inOrder = inOrder(orchestrator, nodeRepository); inOrder.verify(nodeRepository).updateNodeAttributes(hostName, restartGeneration, dockerImage, vespaVersion); @@ -120,12 +117,11 @@ public class NodeAgentImplTest { final String vespaVersion = "7.8.9"; when(nodeRepository.getContainerNodeSpec(hostName)).thenReturn(Optional.of(nodeSpec)); when(docker.getContainer(hostName)).thenReturn(Optional.of(existingContainer)).thenReturn(Optional.empty()); - ; - when(docker.imageIsDownloaded(dockerImage)).thenReturn(true); when(docker.executeInContainer(eq(containerName), anyVararg())).thenReturn(NODE_PROGRAM_DOESNT_EXIST); - when(docker.getVespaVersion(containerName)).thenReturn(vespaVersion); + when(docker.executeInContainer(eq(containerName), eq(DockerOperations.GET_VESPA_VERSION_COMMAND))) + .thenReturn(new ProcessResult(0, vespaVersion, "")); when(orchestrator.suspend(any(HostName.class))).thenReturn(true); nodeAgent.tick(); @@ -135,14 +131,10 @@ public class NodeAgentImplTest { inOrder.verify(docker, times(1)).executeInContainer(any(), anyVararg()); inOrder.verify(docker).stopContainer(containerName); inOrder.verify(docker).deleteContainer(containerName); - inOrder.verify(docker).startContainer( - eq(nodeSpec.wantedDockerImage.get()), - eq(nodeSpec.hostname), - eq(nodeSpec.containerName), - any(InetAddress.class), - eq(nodeSpec.minCpuCores.get()), - eq(nodeSpec.minDiskAvailableGb.get()), - eq(nodeSpec.minMainMemoryAvailableGb.get())); + inOrder.verify(docker).createStartContainerCommand( + nodeSpec.wantedDockerImage.get(), + nodeSpec.containerName, + nodeSpec.hostname); inOrder.verify(docker, times(1)).executeInContainer(any(), anyVararg()); inOrder.verify(nodeRepository).updateNodeAttributes(hostName, wantedRestartGeneration, dockerImage, vespaVersion); @@ -172,7 +164,8 @@ public class NodeAgentImplTest { when(docker.imageIsDownloaded(wantedDockerImage)).thenReturn(true); when(docker.executeInContainer(eq(containerName), anyVararg())).thenReturn(NODE_PROGRAM_DOESNT_EXIST); - when(docker.getVespaVersion(containerName)).thenReturn(vespaVersion); + when(docker.executeInContainer(eq(containerName), eq(DockerOperations.GET_VESPA_VERSION_COMMAND))) + .thenReturn(new ProcessResult(0, vespaVersion, "")); when(orchestrator.suspend(any(HostName.class))).thenReturn(true); when(docker.getContainer(hostName)).thenReturn(Optional.of(existingContainer)).thenReturn(Optional.empty()); @@ -184,14 +177,10 @@ public class NodeAgentImplTest { inOrder.verify(orchestrator).suspend(hostName); inOrder.verify(docker).stopContainer(containerName); inOrder.verify(docker).deleteContainer(containerName); - inOrder.verify(docker).startContainer( - eq(nodeSpec.wantedDockerImage.get()), - eq(nodeSpec.hostname), - eq(nodeSpec.containerName), - any(InetAddress.class), - eq(nodeSpec.minCpuCores.get()), - eq(nodeSpec.minDiskAvailableGb.get()), - eq(nodeSpec.minMainMemoryAvailableGb.get())); + inOrder.verify(docker).createStartContainerCommand( + nodeSpec.wantedDockerImage.get(), + nodeSpec.containerName, + nodeSpec.hostname); inOrder.verify(docker, times(1)).executeInContainer(any(), anyVararg()); inOrder.verify(nodeRepository).updateNodeAttributes(hostName, restartGeneration, wantedDockerImage, vespaVersion); inOrder.verify(orchestrator).resume(hostName); @@ -252,8 +241,13 @@ public class NodeAgentImplTest { when(docker.imageIsDownloaded(dockerImage)).thenReturn(true); when(docker.executeInContainer(eq(containerName), anyVararg())).thenReturn(NODE_PROGRAM_DOESNT_EXIST); - when(docker.getVespaVersion(containerName)).thenReturn(vespaVersion); + when(docker.executeInContainer(eq(containerName), eq(DockerOperations.GET_VESPA_VERSION_COMMAND))) + .thenReturn(new ProcessResult(0, vespaVersion, "")); when(orchestrator.suspend(any(HostName.class))).thenReturn(true); + when(docker.createStartContainerCommand( + nodeSpec.wantedDockerImage.get(), + nodeSpec.containerName, + nodeSpec.hostname)).thenReturn(new DockerMock.StartContainerCommandMock()); nodeAgent.tick(); @@ -262,14 +256,10 @@ public class NodeAgentImplTest { verify(docker, times(1)).executeInContainer(any(), anyVararg()); final InOrder inOrder = inOrder(orchestrator, docker, nodeRepository); inOrder.verify(docker).deleteContainer(containerName); - inOrder.verify(docker).startContainer( - eq(nodeSpec.wantedDockerImage.get()), - eq(nodeSpec.hostname), - eq(nodeSpec.containerName), - any(InetAddress.class), - eq(nodeSpec.minCpuCores.get()), - eq(nodeSpec.minDiskAvailableGb.get()), - eq(nodeSpec.minMainMemoryAvailableGb.get())); + inOrder.verify(docker).createStartContainerCommand( + nodeSpec.wantedDockerImage.get(), + nodeSpec.containerName, + nodeSpec.hostname); inOrder.verify(nodeRepository).updateNodeAttributes(hostName, restartGeneration, dockerImage, vespaVersion); inOrder.verify(orchestrator).resume(hostName); } @@ -294,7 +284,8 @@ public class NodeAgentImplTest { when(docker.imageIsDownloaded(dockerImage)).thenReturn(true); when(docker.executeInContainer(eq(containerName), anyVararg())).thenReturn(NODE_PROGRAM_DOESNT_EXIST); - when(docker.getVespaVersion(containerName)).thenReturn(vespaVersion); + when(docker.executeInContainer(eq(containerName), eq(DockerOperations.GET_VESPA_VERSION_COMMAND))) + .thenReturn(new ProcessResult(0, vespaVersion, "")); when(orchestrator.suspend(any(HostName.class))).thenReturn(true); when(nodeRepository.getContainerNodeSpec(hostName)).thenReturn(Optional.of(nodeSpec)); when(docker.getContainer(hostName)).thenReturn(Optional.empty()); @@ -306,14 +297,10 @@ public class NodeAgentImplTest { verify(docker, times(1)).executeInContainer(any(), anyVararg()); verify(orchestrator, never()).suspend(any(HostName.class)); final InOrder inOrder = inOrder(orchestrator, docker, nodeRepository); - inOrder.verify(docker).startContainer( - eq(nodeSpec.wantedDockerImage.get()), - eq(nodeSpec.hostname), - eq(nodeSpec.containerName), - any(InetAddress.class), - eq(nodeSpec.minCpuCores.get()), - eq(nodeSpec.minDiskAvailableGb.get()), - eq(nodeSpec.minMainMemoryAvailableGb.get())); + inOrder.verify(docker).createStartContainerCommand( + nodeSpec.wantedDockerImage.get(), + nodeSpec.containerName, + nodeSpec.hostname); inOrder.verify(nodeRepository).updateNodeAttributes(hostName, restartGeneration, dockerImage, vespaVersion); inOrder.verify(orchestrator).resume(hostName); } @@ -351,14 +338,10 @@ public class NodeAgentImplTest { verify(orchestrator).suspend(hostName); verify(docker, never()).stopContainer(any(ContainerName.class)); verify(docker, never()).deleteContainer(any(ContainerName.class)); - verify(docker, never()).startContainer( + verify(docker, never()).createStartContainerCommand( any(DockerImage.class), - any(HostName.class), any(ContainerName.class), - any(InetAddress.class), - anyDouble(), - anyDouble(), - anyDouble()); + any(HostName.class)); verify(orchestrator, never()).resume(any(HostName.class)); verify(nodeRepository, never()).updateNodeAttributes( any(HostName.class), anyLong(), any(DockerImage.class), anyString()); @@ -393,14 +376,10 @@ public class NodeAgentImplTest { final InOrder inOrder = inOrder(orchestrator, docker); inOrder.verify(docker).stopContainer(containerName); inOrder.verify(docker).deleteContainer(containerName); - verify(docker, never()).startContainer( + verify(docker, never()).createStartContainerCommand( any(DockerImage.class), - any(HostName.class), any(ContainerName.class), - any(InetAddress.class), - anyDouble(), - anyDouble(), - anyDouble()); + any(HostName.class)); verify(maintenanceScheduler, never()).deleteContainerStorage(any(ContainerName.class)); verify(orchestrator, never()).resume(any(HostName.class)); verify(nodeRepository, never()).updateNodeAttributes( @@ -435,14 +414,10 @@ public class NodeAgentImplTest { verify(orchestrator, never()).suspend(any(HostName.class)); verify(docker, never()).stopContainer(any(ContainerName.class)); verify(docker).deleteContainer(containerName); - verify(docker, never()).startContainer( + verify(docker, never()).createStartContainerCommand( any(DockerImage.class), - any(HostName.class), any(ContainerName.class), - any(InetAddress.class), - anyDouble(), - anyDouble(), - anyDouble()); + any(HostName.class)); verify(maintenanceScheduler, never()).deleteContainerStorage(any(ContainerName.class)); verify(orchestrator, never()).resume(any(HostName.class)); verify(nodeRepository, never()).updateNodeAttributes( @@ -475,14 +450,10 @@ public class NodeAgentImplTest { verify(orchestrator, never()).suspend(any(HostName.class)); verify(docker, never()).stopContainer(any(ContainerName.class)); verify(docker, never()).deleteContainer(containerName); - verify(docker, never()).startContainer( + verify(docker, never()).createStartContainerCommand( any(DockerImage.class), - any(HostName.class), any(ContainerName.class), - any(InetAddress.class), - anyDouble(), - anyDouble(), - anyDouble()); + any(HostName.class)); verify(maintenanceScheduler, never()).deleteContainerStorage(any(ContainerName.class)); verify(orchestrator, never()).resume(any(HostName.class)); verify(nodeRepository, never()).updateNodeAttributes( @@ -518,14 +489,10 @@ public class NodeAgentImplTest { final InOrder inOrder = inOrder(orchestrator, docker); inOrder.verify(docker).stopContainer(containerName); inOrder.verify(docker).deleteContainer(containerName); - verify(docker, never()).startContainer( + verify(docker, never()).createStartContainerCommand( any(DockerImage.class), - any(HostName.class), any(ContainerName.class), - any(InetAddress.class), - anyDouble(), - anyDouble(), - anyDouble()); + any(HostName.class)); verify(maintenanceScheduler, never()).deleteContainerStorage(any(ContainerName.class)); verify(orchestrator, never()).resume(any(HostName.class)); verify(nodeRepository, never()).updateNodeAttributes( @@ -562,14 +529,10 @@ public class NodeAgentImplTest { verify(orchestrator, never()).suspend(any(HostName.class)); verify(docker, never()).stopContainer(any(ContainerName.class)); verify(docker).deleteContainer(containerName); - verify(docker, never()).startContainer( + verify(docker, never()).createStartContainerCommand( any(DockerImage.class), - any(HostName.class), any(ContainerName.class), - any(InetAddress.class), - anyDouble(), - anyDouble(), - anyDouble()); + any(HostName.class)); verify(maintenanceScheduler, never()).deleteContainerStorage(any(ContainerName.class)); verify(orchestrator, never()).resume(any(HostName.class)); verify(nodeRepository, never()).updateNodeAttributes( @@ -605,14 +568,10 @@ public class NodeAgentImplTest { verify(orchestrator, never()).suspend(any(HostName.class)); verify(docker, never()).stopContainer(any(ContainerName.class)); verify(docker, never()).deleteContainer(any(ContainerName.class)); - verify(docker, never()).startContainer( + verify(docker, never()).createStartContainerCommand( any(DockerImage.class), - any(HostName.class), any(ContainerName.class), - any(InetAddress.class), - anyDouble(), - anyDouble(), - anyDouble()); + any(HostName.class)); verify(maintenanceScheduler, never()).deleteContainerStorage(any(ContainerName.class)); verify(orchestrator, never()).resume(any(HostName.class)); verify(nodeRepository, never()).updateNodeAttributes( @@ -652,14 +611,10 @@ public class NodeAgentImplTest { inOrder.verify(docker).deleteContainer(containerName); inOrder.verify(maintenanceScheduler).deleteContainerStorage(containerName); inOrder.verify(nodeRepository).markAsReady(hostName); - verify(docker, never()).startContainer( + verify(docker, never()).createStartContainerCommand( any(DockerImage.class), - any(HostName.class), any(ContainerName.class), - any(InetAddress.class), - anyDouble(), - anyDouble(), - anyDouble()); + any(HostName.class)); verify(orchestrator, never()).resume(any(HostName.class)); verify(nodeRepository, never()).updateNodeAttributes( any(HostName.class), anyLong(), any(DockerImage.class), anyString()); @@ -698,14 +653,10 @@ public class NodeAgentImplTest { inOrder.verify(docker).deleteContainer(containerName); inOrder.verify(maintenanceScheduler).deleteContainerStorage(containerName); inOrder.verify(nodeRepository).markAsReady(hostName); - verify(docker, never()).startContainer( + verify(docker, never()).createStartContainerCommand( any(DockerImage.class), - any(HostName.class), any(ContainerName.class), - any(InetAddress.class), - anyDouble(), - anyDouble(), - anyDouble()); + any(HostName.class)); verify(orchestrator, never()).resume(any(HostName.class)); verify(nodeRepository, never()).updateNodeAttributes( any(HostName.class), anyLong(), any(DockerImage.class), anyString()); @@ -741,14 +692,10 @@ public class NodeAgentImplTest { final InOrder inOrder = inOrder(docker, nodeRepository, maintenanceScheduler); inOrder.verify(maintenanceScheduler).deleteContainerStorage(containerName); inOrder.verify(nodeRepository).markAsReady(hostName); - verify(docker, never()).startContainer( + verify(docker, never()).createStartContainerCommand( any(DockerImage.class), - any(HostName.class), any(ContainerName.class), - any(InetAddress.class), - anyDouble(), - anyDouble(), - anyDouble()); + any(HostName.class)); verify(orchestrator, never()).resume(any(HostName.class)); verify(nodeRepository, never()).updateNodeAttributes( any(HostName.class), anyLong(), any(DockerImage.class), anyString()); @@ -785,14 +732,10 @@ public class NodeAgentImplTest { final InOrder inOrder = inOrder(docker, nodeRepository, maintenanceScheduler); inOrder.verify(maintenanceScheduler).deleteContainerStorage(containerName); inOrder.verify(nodeRepository).markAsReady(hostName); - verify(docker, never()).startContainer( + verify(docker, never()).createStartContainerCommand( any(DockerImage.class), - any(HostName.class), any(ContainerName.class), - any(InetAddress.class), - anyDouble(), - anyDouble(), - anyDouble()); + any(HostName.class)); verify(orchestrator, never()).resume(any(HostName.class)); verify(nodeRepository, never()).updateNodeAttributes( any(HostName.class), anyLong(), any(DockerImage.class), anyString()); @@ -831,7 +774,8 @@ public class NodeAgentImplTest { when(docker.imageIsDownloaded(any(DockerImage.class))).thenReturn(true); when(docker.executeInContainer(eq(containerName), anyVararg())).thenReturn(NODE_PROGRAM_DOESNT_EXIST); - when(docker.getVespaVersion(containerName)).thenReturn(vespaVersion); + when(docker.executeInContainer(eq(containerName), eq(DockerOperations.GET_VESPA_VERSION_COMMAND))) + .thenReturn(new ProcessResult(0, vespaVersion, "")); when(orchestrator.suspend(any(HostName.class))).thenReturn(true); final InOrder inOrder = inOrder(nodeRepository, docker); @@ -908,7 +852,8 @@ public class NodeAgentImplTest { when(docker.imageIsDownloaded(any(DockerImage.class))).thenReturn(true); when(docker.executeInContainer(eq(containerName), anyVararg())).thenReturn(NODE_PROGRAM_DOESNT_EXIST); - when(docker.getVespaVersion(containerName)).thenReturn(vespaVersion); + when(docker.executeInContainer(eq(containerName), eq(DockerOperations.GET_VESPA_VERSION_COMMAND))) + .thenReturn(new ProcessResult(0, vespaVersion, "")); when(orchestrator.suspend(any(HostName.class))).thenReturn(true); doThrow(new IOException()).doNothing().when(nodeRepository).updateNodeAttributes( any(HostName.class), anyLong(), any(DockerImage.class), anyString()); @@ -967,7 +912,8 @@ public class NodeAgentImplTest { .thenReturn(new ProcessResult(0, "node program exists", "")) .thenReturn(new ProcessResult(0, "node program succeeds 3rd time", "")); - when(docker.getVespaVersion(containerName)).thenReturn(vespaVersion); + when(docker.executeInContainer(eq(containerName), eq(DockerOperations.GET_VESPA_VERSION_COMMAND))) + .thenReturn(new ProcessResult(0, vespaVersion, "")); final InOrder inOrder = inOrder(orchestrator, docker); @@ -980,14 +926,10 @@ public class NodeAgentImplTest { fail("Expected to throw an exception"); } catch (Exception e) { } - inOrder.verify(docker).startContainer( - eq(nodeSpec.wantedDockerImage.get()), - eq(nodeSpec.hostname), - eq(nodeSpec.containerName), - any(InetAddress.class), - eq(nodeSpec.minCpuCores.get()), - eq(nodeSpec.minDiskAvailableGb.get()), - eq(nodeSpec.minMainMemoryAvailableGb.get())); + inOrder.verify(docker).createStartContainerCommand( + nodeSpec.wantedDockerImage.get(), + nodeSpec.containerName, + nodeSpec.hostname); inOrder.verify(docker, times(2)).executeInContainer(any(), anyVararg()); inOrder.verifyNoMoreInteractions(); @@ -1064,7 +1006,8 @@ public class NodeAgentImplTest { .thenReturn(new ProcessResult(0, "output", "")); // resuming succeeds break; } - when(docker.getVespaVersion(containerName)).thenReturn(vespaVersion); + when(docker.executeInContainer(eq(containerName), eq(DockerOperations.GET_VESPA_VERSION_COMMAND))) + .thenReturn(new ProcessResult(0, vespaVersion, "")); when(orchestrator.suspend(any(HostName.class))).thenReturn(true); when(docker.getContainer(hostName)).thenReturn(Optional.of(existingContainer)).thenReturn(Optional.empty()); @@ -1087,14 +1030,10 @@ public class NodeAgentImplTest { inOrder.verify(docker).stopContainer(containerName); inOrder.verify(docker).deleteContainer(containerName); - inOrder.verify(docker).startContainer( - eq(nodeSpec.wantedDockerImage.get()), - eq(nodeSpec.hostname), - eq(nodeSpec.containerName), - any(InetAddress.class), - eq(nodeSpec.minCpuCores.get()), - eq(nodeSpec.minDiskAvailableGb.get()), - eq(nodeSpec.minMainMemoryAvailableGb.get())); + inOrder.verify(docker).createStartContainerCommand( + nodeSpec.wantedDockerImage.get(), + nodeSpec.containerName, + nodeSpec.hostname); switch (scenario) { case EXCEPTION: diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImplTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImplTest.java index 5c707c56bc8..16e41d601fb 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImplTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImplTest.java @@ -7,8 +7,8 @@ import com.yahoo.application.Networking; import com.yahoo.application.container.JDisc; import com.yahoo.vespa.applicationmodel.HostName; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; -import com.yahoo.vespa.hosted.node.admin.docker.DockerImage; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.DockerImage; import com.yahoo.vespa.hosted.provision.testutils.ContainerConfig; import org.junit.After; diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/maintenance/MaintainerTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/maintenance/MaintainerTest.java index 0c46f4876cc..979b23dbfcb 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/maintenance/MaintainerTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/maintenance/MaintainerTest.java @@ -1,6 +1,6 @@ package com.yahoo.vespa.hosted.node.maintenance; -import com.yahoo.vespa.hosted.node.admin.docker.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; import org.junit.Test; import static org.junit.Assert.assertEquals; |