From bef662b9743999cce4e71afa3f331b2b9bdfa5be Mon Sep 17 00:00:00 2001 From: Martin Polden Date: Mon, 28 Jun 2021 11:52:26 +0200 Subject: Remove docker-api --- docker-api/.gitignore | 1 - docker-api/CMakeLists.txt | 2 - docker-api/OWNERS | 1 - docker-api/pom.xml | 150 ------- .../yahoo/vespa/hosted/dockerapi/Container.java | 81 ---- .../vespa/hosted/dockerapi/ContainerEngine.java | 99 ----- .../yahoo/vespa/hosted/dockerapi/ContainerId.java | 36 -- .../vespa/hosted/dockerapi/ContainerName.java | 55 --- .../vespa/hosted/dockerapi/ContainerResources.java | 131 ------ .../vespa/hosted/dockerapi/ContainerStats.java | 232 ---------- .../dockerapi/CreateContainerCommandImpl.java | 263 ----------- .../yahoo/vespa/hosted/dockerapi/DockerEngine.java | 479 --------------------- .../dockerapi/DockerImageGarbageCollector.java | 200 --------- .../vespa/hosted/dockerapi/ProcessResult.java | 47 -- .../hosted/dockerapi/RegistryCredentials.java | 58 --- .../exception/ContainerNotFoundException.java | 13 - .../dockerapi/exception/DockerException.java | 16 - .../exception/DockerExecTimeoutException.java | 17 - .../hosted/dockerapi/exception/package-info.java | 5 - .../vespa/hosted/dockerapi/metrics/Counter.java | 28 -- .../hosted/dockerapi/metrics/DimensionMetrics.java | 76 ---- .../vespa/hosted/dockerapi/metrics/Dimensions.java | 55 --- .../vespa/hosted/dockerapi/metrics/Gauge.java | 24 -- .../hosted/dockerapi/metrics/MetricValue.java | 9 - .../vespa/hosted/dockerapi/metrics/Metrics.java | 128 ------ .../hosted/dockerapi/metrics/package-info.java | 5 - .../yahoo/vespa/hosted/dockerapi/package-info.java | 5 - .../vespa/hosted/dockerapi/ContainerNameTest.java | 45 -- .../hosted/dockerapi/ContainerResourcesTest.java | 49 --- .../dockerapi/CreateContainerCommandImplTest.java | 64 --- .../vespa/hosted/dockerapi/DockerEngineTest.java | 147 ------- .../DockerImageGarbageCollectionTest.java | 284 ------------ .../vespa/hosted/dockerapi/ProcessResultTest.java | 24 -- .../hosted/dockerapi/metrics/MetricsTest.java | 99 ----- 34 files changed, 2928 deletions(-) delete mode 100644 docker-api/.gitignore delete mode 100644 docker-api/CMakeLists.txt delete mode 100644 docker-api/OWNERS delete mode 100644 docker-api/pom.xml delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/Container.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerEngine.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerId.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerName.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerResources.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerStats.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/CreateContainerCommandImpl.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/DockerEngine.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/DockerImageGarbageCollector.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ProcessResult.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/RegistryCredentials.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/exception/ContainerNotFoundException.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/exception/DockerException.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/exception/DockerExecTimeoutException.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/exception/package-info.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/Counter.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/DimensionMetrics.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/Dimensions.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/Gauge.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/MetricValue.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/Metrics.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/package-info.java delete mode 100644 docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/package-info.java delete mode 100644 docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/ContainerNameTest.java delete mode 100644 docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/ContainerResourcesTest.java delete mode 100644 docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/CreateContainerCommandImplTest.java delete mode 100644 docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/DockerEngineTest.java delete mode 100644 docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/DockerImageGarbageCollectionTest.java delete mode 100644 docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/ProcessResultTest.java delete mode 100644 docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/metrics/MetricsTest.java (limited to 'docker-api') diff --git a/docker-api/.gitignore b/docker-api/.gitignore deleted file mode 100644 index 1d1fe94df49..00000000000 --- a/docker-api/.gitignore +++ /dev/null @@ -1 +0,0 @@ -Dockerfile \ No newline at end of file diff --git a/docker-api/CMakeLists.txt b/docker-api/CMakeLists.txt deleted file mode 100644 index edcdcfb5bfc..00000000000 --- a/docker-api/CMakeLists.txt +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -install(FILES target/docker-api-jar-with-dependencies.jar DESTINATION conf/node-admin-app/components) diff --git a/docker-api/OWNERS b/docker-api/OWNERS deleted file mode 100644 index e030acdbc5b..00000000000 --- a/docker-api/OWNERS +++ /dev/null @@ -1 +0,0 @@ -freva diff --git a/docker-api/pom.xml b/docker-api/pom.xml deleted file mode 100644 index 749eca97c53..00000000000 --- a/docker-api/pom.xml +++ /dev/null @@ -1,150 +0,0 @@ - - - - 4.0.0 - - com.yahoo.vespa - parent - 7-SNAPSHOT - ../parent/pom.xml - - - docker-api - 7-SNAPSHOT - container-plugin - ${project.artifactId} - - - - - com.yahoo.vespa - container-dev - ${project.version} - provided - - - com.yahoo.vespa - config-provisioning - ${project.version} - provided - - - - - com.github.docker-java - docker-java - 3.1.2 - compile - - - org.slf4j - slf4j-api - - - com.google.guava - guava - - - com.fasterxml.jackson.jaxrs - jackson-jaxrs-json-provider - - - com.fasterxml.jackson.core - jackson-databind - - - org.glassfish.jersey.core - jersey-client - - - org.glassfish.jersey.core - jersey-common - - - javax.ws.rs - javax.ws.rs-api - - - com.fasterxml.jackson.core - jackson-core - - - com.google.code.findbugs - annotations - - - org.apache.httpcomponents - httpcore - - - org.apache.httpcomponents - httpclient - - - org.bouncycastle - bcpkix-jdk15on - - - org.bouncycastle - bcprov-jdk15on - - - - - org.apache.httpcomponents - httpcore - - 4.4.1 - compile - - - org.apache.httpcomponents - httpclient - - 4.5 - compile - - - - - junit - junit - test - - - org.mockito - mockito-core - test - - - com.yahoo.vespa - testutil - ${project.version} - test - - - - - - - com.yahoo.vespa - bundle-plugin - true - - true - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/Container.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/Container.java deleted file mode 100644 index 9e304e5ef4d..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/Container.java +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi; - -import com.yahoo.config.provision.DockerImage; - -import java.util.Objects; - -/** - * @author stiankri - */ -// TODO: Move this to node-admin when docker-api module can be removed -public class Container { - private final ContainerId id; - public final String hostname; - public final DockerImage image; - public final ContainerResources resources; - public final ContainerName name; - public final State state; - public final int pid; - - public Container( - final ContainerId id, - final String hostname, - final DockerImage image, - final ContainerResources resources, - final ContainerName containerName, - final State state, - final int pid) { - this.id = id; - this.hostname = hostname; - this.image = image; - this.resources = resources; - this.name = containerName; - this.state = state; - this.pid = pid; - } - - public ContainerId id() { - return id; - } - - @Override - public boolean equals(final Object obj) { - if (!(obj instanceof Container)) { - return false; - } - final Container other = (Container) obj; - return Objects.equals(id, other.id) - && Objects.equals(hostname, other.hostname) - && Objects.equals(image, other.image) - && Objects.equals(resources, other.resources) - && Objects.equals(name, other.name) - && Objects.equals(pid, other.pid); - } - - @Override - public int hashCode() { - return Objects.hash(hostname, image, resources, name, pid); - } - - @Override - public String toString() { - return "Container {" - + " id=" + id - + " hostname=" + hostname - + " image=" + image - + " resources=" + resources - + " name=" + name - + " state=" + state - + " pid=" + pid - + "}"; - } - - public enum State { - CREATED, RESTARTING, RUNNING, REMOVING, PAUSED, EXITED, DEAD, UNKNOWN, CONFIGURED, STOPPED; - - public boolean isRunning() { - return this == RUNNING; - } - } -} diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerEngine.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerEngine.java deleted file mode 100644 index 7a8d98f0e85..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerEngine.java +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi; - -import com.yahoo.config.provision.DockerImage; - -import java.net.InetAddress; -import java.nio.file.Path; -import java.time.Duration; -import java.util.List; -import java.util.Optional; -import java.util.OptionalLong; - -/** - * API that hides the access to a specific container engine like Docker or Podman. - */ -public interface ContainerEngine { - - interface CreateContainerCommand { - CreateContainerCommand withHostName(String hostname); - CreateContainerCommand withResources(ContainerResources containerResources); - CreateContainerCommand withLabel(String name, String value); - CreateContainerCommand withEnvironment(String name, String value); - - /** - * Mounts a directory on host inside the container. - * - *

Bind mount content will be private to this container (and host) only. - * - *

When using this method and selinux is enabled (/usr/sbin/sestatus), starting - * multiple containers which mount host's /foo directory into the container, will make - * /foo's content visible/readable/writable only inside the container which was last - * started and on the host. All the other containers will get "Permission denied". - * - *

Use {@link #withSharedVolume(Path, Path)} to mount a given host directory - * into multiple containers. - */ - CreateContainerCommand withVolume(Path path, Path volumePath); - - /** - * Mounts a directory on host inside the container. - * - *

The bind mount content will be shared among multiple containers. - * - * @see #withVolume(Path, Path) - */ - CreateContainerCommand withSharedVolume(Path path, Path volumePath); - CreateContainerCommand withNetworkMode(String mode); - CreateContainerCommand withIpAddress(InetAddress address); - CreateContainerCommand withUlimit(String name, int softLimit, int hardLimit); - CreateContainerCommand withEntrypoint(String... entrypoint); - CreateContainerCommand withManagedBy(String manager); - CreateContainerCommand withAddCapability(String capabilityName); - CreateContainerCommand withDropCapability(String capabilityName); - CreateContainerCommand withSecurityOpt(String securityOpt); - CreateContainerCommand withDnsOption(String dnsOption); - CreateContainerCommand withPrivileged(boolean privileged); - - void create(); - } - - CreateContainerCommand createContainerCommand(DockerImage dockerImage, ContainerName containerName); - - Optional getContainerStats(ContainerName containerName); - - void startContainer(ContainerName containerName); - - void stopContainer(ContainerName containerName); - - void deleteContainer(ContainerName containerName); - - void updateContainer(ContainerName containerName, ContainerResources containerResources); - - Optional getContainer(ContainerName containerName); - - /** - * Checks if the image is currently being pulled or is already pulled, if not, starts an async - * pull of the image - * - * @param image Docker image to pull - * @return true iff image being pulled, false otherwise - */ - boolean pullImageAsyncIfNeeded(DockerImage image, RegistryCredentials registryCredentials); - - boolean noManagedContainersRunning(String manager); - - List listManagedContainers(String manager); - - boolean deleteUnusedDockerImages(List excludes, Duration minImageAgeToDelete); - - /** - * @param containerName The name of the container - * @param user can be "username", "username:group", "uid" or "uid:gid" - * @param timeoutSeconds Timeout for the process to finish in seconds or without timeout if empty - * @param command The command with arguments to run - * - * @return exitcodes, stdout and stderr in the ProcessResult - */ - ProcessResult executeInContainerAsUser(ContainerName containerName, String user, OptionalLong timeoutSeconds, String... command); -} diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerId.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerId.java deleted file mode 100644 index b86238324f0..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerId.java +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -// -package com.yahoo.vespa.hosted.dockerapi; - -import java.util.Objects; - -/** - * The ID of a container. - * - * @author hakon - */ -public class ContainerId { - private final String id; - - public ContainerId(String id) { - this.id = Objects.requireNonNull(id, "id cannot be null"); - } - - @Override - public String toString() { - return id; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ContainerId that = (ContainerId) o; - return id.equals(that.id); - } - - @Override - public int hashCode() { - return Objects.hash(id); - } -} diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerName.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerName.java deleted file mode 100644 index 53bfc59652c..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerName.java +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi; - -import java.util.Objects; -import java.util.regex.Pattern; - -/** - * Type-safe value wrapper for docker container names. - * - * @author bakksjo - */ -// TODO: Move this to node-admin when docker-api module can be removed -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; - } - - public static ContainerName fromHostname(final String hostName) { - return new ContainerName(hostName.split("\\.", 2)[0]); - } - - @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/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerResources.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerResources.java deleted file mode 100644 index 37e265bf411..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerResources.java +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi; - -import java.util.Objects; - -/** - * @author valerijf - */ -// TODO: Move this to node-admin when docker-api module can be removed -public class ContainerResources { - - public static final ContainerResources UNLIMITED = ContainerResources.from(0, 0, 0); - public static final int CPU_PERIOD_US = 100_000; // 100 ms - - /** - * Hard limit on container's CPU usage: Implemented using Completely Fair Scheduler (CFS) by allocating a given - * time within a given period, Container's processes are not bound to any specific CPU, which may create significant - * performance degradation as processes are scheduled on another CPU after exhausting the quota. - */ - private final double cpus; - - /** - * Soft limit on container's CPU usage: When plenty of CPU cycles are available, all containers use as much - * CPU as they need. It prioritizes container CPU resources for the available CPU cycles. - * It does not guarantee or reserve any specific CPU access. - */ - private final int cpuShares; - - /** The maximum amount, in bytes, of memory the container can use. */ - private final long memoryBytes; - - public ContainerResources(double cpus, int cpuShares, long memoryBytes) { - this.cpus = cpus; - this.cpuShares = cpuShares; - this.memoryBytes = memoryBytes; - - if (cpus < 0) - throw new IllegalArgumentException("CPUs must be a positive number or 0 for unlimited, was " + cpus); - if (cpuShares != 0 && (cpuShares < 2 || cpuShares > 262_144)) - throw new IllegalArgumentException("CPU shares must be a positive integer in [2, 262144] or 0 for unlimited, was " + cpuShares); - if (memoryBytes < 0) - throw new IllegalArgumentException("memoryBytes must be a positive integer or 0 for unlimited, was " + memoryBytes); - } - - /** - * Create container resources from required fields. - * - * @param maxVcpu the amount of vcpu that allocation policies should allocate exclusively to this container. - * This is a hard upper limit. To allow an unlimited amount use 0. - * @param minVcpu the minimal amount of vcpu dedicated to this container. - * To avoid dedicating any cpu at all, use 0. - * @param memoryGb the amount of memory that allocation policies should allocate to this container. - * This is a hard upper limit. To allow the container to allocate an unlimited amount use 0. - * @return the container resources encapsulating the parameters - */ - public static ContainerResources from(double maxVcpu, double minVcpu, double memoryGb) { - return new ContainerResources(maxVcpu, - (int) Math.round(32 * minVcpu), - (long) ((1L << 30) * memoryGb)); - } - - public double cpus() { - return cpus; - } - - /** Returns the CFS CPU quota per {@link #cpuPeriod()}, or -1 if disabled. */ - public int cpuQuota() { - return cpus > 0 ? (int) (cpus * CPU_PERIOD_US) : -1; - } - - /** Duration (in µs) of a single period used as the basis for process scheduling */ - public int cpuPeriod() { - return CPU_PERIOD_US; - } - - public int cpuShares() { - return cpuShares; - } - - public long memoryBytes() { - return memoryBytes; - } - - /** Returns true iff the memory component(s) of between this and other are equal */ - public boolean equalsMemory(ContainerResources other) { - return memoryBytes == other.memoryBytes; - } - - /** Returns true iff the CPU component(s) of between this and other are equal */ - public boolean equalsCpu(ContainerResources other) { - return Math.abs(other.cpus - cpus) < 0.0001 && cpuShares == other.cpuShares; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ContainerResources that = (ContainerResources) o; - return equalsMemory(that) && equalsCpu(that); - } - - @Override - public int hashCode() { - return Objects.hash(cpus, cpuShares, memoryBytes); - } - - - /** Returns only the memory component(s) of {@link #toString()} */ - public String toStringMemory() { - return (memoryBytes > 0 ? memoryBytes + "B" : "unlimited") + " memory"; - } - - /** Returns only the CPU component(s) of {@link #toString()} */ - public String toStringCpu() { - return (cpus > 0 ? String.format("%.2f", cpus) : "unlimited") +" CPUs, " + - (cpuShares > 0 ? cpuShares : "unlimited") + " CPU Shares"; - } - - @Override - public String toString() { - return toStringCpu() + ", " + toStringMemory(); - } - - public ContainerResources withMemoryBytes(long memoryBytes) { - return new ContainerResources(cpus, cpuShares, memoryBytes); - } - - public ContainerResources withUnlimitedCpus() { - return new ContainerResources(0, 0, memoryBytes); - } -} diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerStats.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerStats.java deleted file mode 100644 index dc2db50d3ab..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ContainerStats.java +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Objects; - -/** - * CPU, memory and network statistics collected from a container. - * - * @author freva - */ -// TODO: Move this to node-admin when docker-api module can be removed -public class ContainerStats { - - private final Map networkStatsByInterface; - private final MemoryStats memoryStats; - private final CpuStats cpuStats; - - public ContainerStats(Map networkStatsByInterface, MemoryStats memoryStats, CpuStats cpuStats) { - this.networkStatsByInterface = new LinkedHashMap<>(Objects.requireNonNull(networkStatsByInterface)); - this.memoryStats = Objects.requireNonNull(memoryStats); - this.cpuStats = Objects.requireNonNull(cpuStats); - } - - public Map getNetworks() { - return Collections.unmodifiableMap(networkStatsByInterface); - } - - public MemoryStats getMemoryStats() { - return memoryStats; - } - - public CpuStats getCpuStats() { - return cpuStats; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ContainerStats that = (ContainerStats) o; - return networkStatsByInterface.equals(that.networkStatsByInterface) && memoryStats.equals(that.memoryStats) && cpuStats.equals(that.cpuStats); - } - - @Override - public int hashCode() { - return Objects.hash(networkStatsByInterface, memoryStats, cpuStats); - } - - /** Statistics for network usage */ - public static class NetworkStats { - - private final long rxBytes; - private final long rxDropped; - private final long rxErrors; - private final long txBytes; - private final long txDropped; - private final long txErrors; - - public NetworkStats(long rxBytes, long rxDropped, long rxErrors, long txBytes, long txDropped, long txErrors) { - this.rxBytes = rxBytes; - this.rxDropped = rxDropped; - this.rxErrors = rxErrors; - this.txBytes = txBytes; - this.txDropped = txDropped; - this.txErrors = txErrors; - } - - /** Returns received bytes */ - public long getRxBytes() { return this.rxBytes; } - - /** Returns received bytes, which was dropped */ - public long getRxDropped() { return this.rxDropped; } - - /** Returns received errors */ - public long getRxErrors() { return this.rxErrors; } - - /** Returns transmitted bytes */ - public long getTxBytes() { return this.txBytes; } - - /** Returns transmitted bytes, which was dropped */ - public long getTxDropped() { return this.txDropped; } - - /** Returns transmission errors */ - public long getTxErrors() { return this.txErrors; } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - NetworkStats that = (NetworkStats) o; - return rxBytes == that.rxBytes && rxDropped == that.rxDropped && rxErrors == that.rxErrors && txBytes == that.txBytes && txDropped == that.txDropped && txErrors == that.txErrors; - } - - @Override - public int hashCode() { - return Objects.hash(rxBytes, rxDropped, rxErrors, txBytes, txDropped, txErrors); - } - - @Override - public String toString() { - return "NetworkStats{" + - "rxBytes=" + rxBytes + - ", rxDropped=" + rxDropped + - ", rxErrors=" + rxErrors + - ", txBytes=" + txBytes + - ", txDropped=" + txDropped + - ", txErrors=" + txErrors + - '}'; - } - - } - - /** Statistics for memory usage */ - public static class MemoryStats { - - private final long cache; - private final long usage; - private final long limit; - - public MemoryStats(long cache, long usage, long limit) { - this.cache = cache; - this.usage = usage; - this.limit = limit; - } - - /** Returns memory used by cache in bytes */ - public long getCache() { return this.cache; } - - /** Returns memory usage in bytes */ - public long getUsage() { return this.usage; } - - /** Returns memory limit in bytes */ - public long getLimit() { return this.limit; } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - MemoryStats that = (MemoryStats) o; - return cache == that.cache && usage == that.usage && limit == that.limit; - } - - @Override - public int hashCode() { - return Objects.hash(cache, usage, limit); - } - - @Override - public String toString() { - return "MemoryStats{" + - "cache=" + cache + - ", usage=" + usage + - ", limit=" + limit + - '}'; - } - - } - - /** Statistics for CPU usage */ - public static class CpuStats { - - private final int onlineCpus; - private final long systemCpuUsage; - private final long totalUsage; - private final long usageInKernelMode; - private final long throttledTime; - private final long throttlingActivePeriods; - private final long throttledPeriods; - - public CpuStats(int onlineCpus, long systemCpuUsage, long totalUsage, long usageInKernelMode, - long throttledTime, long throttlingActivePeriods, long throttledPeriods) { - this.onlineCpus = onlineCpus; - this.systemCpuUsage = systemCpuUsage; - this.totalUsage = totalUsage; - this.usageInKernelMode = usageInKernelMode; - this.throttledTime = throttledTime; - this.throttlingActivePeriods = throttlingActivePeriods; - this.throttledPeriods = throttledPeriods; - } - - public int getOnlineCpus() { return this.onlineCpus; } - - /** Total CPU time (in ns) spent executing all the processes on this host */ - public long getSystemCpuUsage() { return this.systemCpuUsage; } - - /** Total CPU time (in ns) spent running all the processes in this container */ - public long getTotalUsage() { return totalUsage; } - - /** Total CPU time (in ns) spent in kernel mode while executing processes in this container */ - public long getUsageInKernelMode() { return usageInKernelMode; } - - /** Total CPU time (in ns) processes in this container were throttled for */ - public long getThrottledTime() { return throttledTime; } - - /** Number of periods with throttling enabled for this container */ - public long getThrottlingActivePeriods() { return throttlingActivePeriods; } - - /** Number of periods this container hit the throttling limit */ - public long getThrottledPeriods() { return throttledPeriods; } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CpuStats cpuStats = (CpuStats) o; - return onlineCpus == cpuStats.onlineCpus && systemCpuUsage == cpuStats.systemCpuUsage && totalUsage == cpuStats.totalUsage && usageInKernelMode == cpuStats.usageInKernelMode && throttledTime == cpuStats.throttledTime && throttlingActivePeriods == cpuStats.throttlingActivePeriods && throttledPeriods == cpuStats.throttledPeriods; - } - - @Override - public int hashCode() { - return Objects.hash(onlineCpus, systemCpuUsage, totalUsage, usageInKernelMode, throttledTime, throttlingActivePeriods, throttledPeriods); - } - - @Override - public String toString() { - return "CpuStats{" + - "onlineCpus=" + onlineCpus + - ", systemCpuUsage=" + systemCpuUsage + - ", totalUsage=" + totalUsage + - ", usageInKernelMode=" + usageInKernelMode + - ", throttledTime=" + throttledTime + - ", throttlingActivePeriods=" + throttlingActivePeriods + - ", throttledPeriods=" + throttledPeriods + - '}'; - } - - } - -} diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/CreateContainerCommandImpl.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/CreateContainerCommandImpl.java deleted file mode 100644 index 7f8a2b68741..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/CreateContainerCommandImpl.java +++ /dev/null @@ -1,263 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi; - -import com.github.dockerjava.api.DockerClient; -import com.github.dockerjava.api.command.CreateContainerCmd; -import com.github.dockerjava.api.model.Bind; -import com.github.dockerjava.api.model.Capability; -import com.github.dockerjava.api.model.HostConfig; -import com.github.dockerjava.api.model.Ulimit; -import com.yahoo.config.provision.DockerImage; -import com.yahoo.vespa.hosted.dockerapi.exception.DockerException; - -import java.net.Inet6Address; -import java.net.InetAddress; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static com.yahoo.vespa.hosted.dockerapi.DockerEngine.LABEL_NAME_MANAGEDBY; - -class CreateContainerCommandImpl implements ContainerEngine.CreateContainerCommand { - - private final DockerClient docker; - private final DockerImage dockerImage; - private final ContainerName containerName; - private final Map labels = new HashMap<>(); - private final List environmentAssignments = new ArrayList<>(); - private final List volumeBindSpecs = new ArrayList<>(); - private final List dnsOptions = new ArrayList<>(); - private final List ulimits = new ArrayList<>(); - private final Set addCapabilities = new HashSet<>(); - private final Set dropCapabilities = new HashSet<>(); - private final Set securityOpts = new HashSet<>(); - - private Optional hostName = Optional.empty(); - private Optional containerResources = Optional.empty(); - private Optional networkMode = Optional.empty(); - private Optional ipv4Address = Optional.empty(); - private Optional ipv6Address = Optional.empty(); - private Optional entrypoint = Optional.empty(); - private boolean privileged = false; - - CreateContainerCommandImpl(DockerClient docker, DockerImage dockerImage, ContainerName containerName) { - this.docker = docker; - this.dockerImage = dockerImage; - this.containerName = containerName; - } - - - @Override - public ContainerEngine.CreateContainerCommand withHostName(String hostName) { - this.hostName = Optional.of(hostName); - return this; - } - - @Override - public ContainerEngine.CreateContainerCommand withResources(ContainerResources containerResources) { - this.containerResources = Optional.of(containerResources); - return this; - } - - @Override - public ContainerEngine.CreateContainerCommand withLabel(String name, String value) { - assert !name.contains("="); - labels.put(name, value); - return this; - } - - public ContainerEngine.CreateContainerCommand withManagedBy(String manager) { - return withLabel(LABEL_NAME_MANAGEDBY, manager); - } - - @Override - public ContainerEngine.CreateContainerCommand withAddCapability(String capabilityName) { - addCapabilities.add(Capability.valueOf(capabilityName)); - return this; - } - - @Override - public ContainerEngine.CreateContainerCommand withDropCapability(String capabilityName) { - dropCapabilities.add(Capability.valueOf(capabilityName)); - return this; - } - - @Override - public ContainerEngine.CreateContainerCommand withSecurityOpt(String securityOpt) { - securityOpts.add(securityOpt); - return this; - } - - @Override - public ContainerEngine.CreateContainerCommand withDnsOption(String dnsOption) { - dnsOptions.add(dnsOption); - return this; - } - - @Override - public ContainerEngine.CreateContainerCommand withPrivileged(boolean privileged) { - this.privileged = privileged; - return this; - } - - @Override - public ContainerEngine.CreateContainerCommand withUlimit(String name, int softLimit, int hardLimit) { - ulimits.add(new Ulimit(name, softLimit, hardLimit)); - return this; - } - - @Override - public ContainerEngine.CreateContainerCommand withEntrypoint(String... entrypoint) { - if (entrypoint.length < 1) throw new IllegalArgumentException("Entrypoint must contain at least 1 element"); - this.entrypoint = Optional.of(entrypoint); - return this; - } - - - @Override - public ContainerEngine.CreateContainerCommand withEnvironment(String name, String value) { - assert name.indexOf('=') == -1; - environmentAssignments.add(name + "=" + value); - return this; - } - - @Override - public ContainerEngine.CreateContainerCommand withVolume(Path path, Path volumePath) { - volumeBindSpecs.add(path + ":" + volumePath + ":Z"); - return this; - } - - @Override - public ContainerEngine.CreateContainerCommand withSharedVolume(Path path, Path volumePath) { - volumeBindSpecs.add(path + ":" + volumePath + ":z"); - return this; - } - - @Override - public ContainerEngine.CreateContainerCommand withNetworkMode(String mode) { - networkMode = Optional.of(mode); - return this; - } - - @Override - public ContainerEngine.CreateContainerCommand withIpAddress(InetAddress address) { - if (address instanceof Inet6Address) { - ipv6Address = Optional.of(address.getHostAddress()); - } else { - ipv4Address = Optional.of(address.getHostAddress()); - } - return this; - } - - @Override - public void create() { - try { - createCreateContainerCmd().exec(); - } catch (RuntimeException e) { - throw new DockerException("Failed to create container " + toString(), e); - } - } - - private CreateContainerCmd createCreateContainerCmd() { - List volumeBinds = volumeBindSpecs.stream().map(Bind::parse).collect(Collectors.toList()); - - final HostConfig hostConfig = new HostConfig() - .withSecurityOpts(new ArrayList<>(securityOpts)) - .withBinds(volumeBinds) - .withUlimits(ulimits) - // Docker version 1.13.1 patch 94 changed default pids.max for the Docker container's cgroup - // from max to 4096. -1L reinstates "max". File: /sys/fs/cgroup/pids/docker/CONTAINERID/pids.max. - .withPidsLimit(-1L) - .withCapAdd(addCapabilities.toArray(new Capability[0])) - .withCapDrop(dropCapabilities.toArray(new Capability[0])) - .withDnsOptions(dnsOptions) - .withPrivileged(privileged); - - containerResources.ifPresent(cr -> hostConfig - .withCpuShares(cr.cpuShares()) - .withMemory(cr.memoryBytes()) - // MemorySwap is the total amount of memory and swap, if MemorySwap == Memory, then container has no access swap - .withMemorySwap(cr.memoryBytes()) - .withCpuPeriod(cr.cpuQuota() > 0 ? (long) cr.cpuPeriod() : null) - .withCpuQuota(cr.cpuQuota() > 0 ? (long) cr.cpuQuota() : null)); - - final CreateContainerCmd containerCmd = docker - .createContainerCmd(dockerImage.asString()) - .withHostConfig(hostConfig) - .withName(containerName.asString()) - .withLabels(labels) - .withEnv(environmentAssignments); - - hostName.ifPresent(containerCmd::withHostName); - networkMode.ifPresent(hostConfig::withNetworkMode); - ipv4Address.ifPresent(containerCmd::withIpv4Address); - ipv6Address.ifPresent(containerCmd::withIpv6Address); - entrypoint.ifPresent(containerCmd::withEntrypoint); - - return containerCmd; - } - - /** Maps ("--env", {"A", "B", "C"}) to "--env A --env B --env C" */ - private static String toRepeatedOption(String option, Collection optionValues) { - return optionValues.stream() - .map(optionValue -> option + " " + optionValue) - .collect(Collectors.joining(" ")); - } - - private static String toOptionalOption(String option, Optional value) { - return value.map(o -> option + " " + o).orElse(""); - } - - private static String toFlagOption(String option, boolean value) { - return value ? option : ""; - } - - /** Make toString() print the equivalent arguments to 'docker run' */ - @Override - public String toString() { - List labelList = labels.entrySet().stream() - .map(entry -> entry.getKey() + "=" + entry.getValue()).collect(Collectors.toList()); - List ulimitList = ulimits.stream() - .map(ulimit -> ulimit.getName() + "=" + ulimit.getSoft() + ":" + ulimit.getHard()) - .collect(Collectors.toList()); - List addCapabilitiesList = addCapabilities.stream().map(Enum::toString).sorted().collect(Collectors.toList()); - List dropCapabilitiesList = dropCapabilities.stream().map(Enum::toString).sorted().collect(Collectors.toList()); - Optional entrypointExecuteable = entrypoint.map(args -> args[0]); - String entrypointArgs = entrypoint.map(Stream::of).orElseGet(Stream::empty) - .skip(1) - .collect(Collectors.joining(" ")); - - return Stream.of( - "--name " + containerName.asString(), - toOptionalOption("--hostname", hostName), - toOptionalOption("--cpu-shares", containerResources.map(ContainerResources::cpuShares)), - toOptionalOption("--cpus", containerResources.map(ContainerResources::cpus)), - toOptionalOption("--memory", containerResources.map(ContainerResources::memoryBytes)), - toRepeatedOption("--label", labelList), - toRepeatedOption("--ulimit", ulimitList), - "--pids-limit -1", - toRepeatedOption("--env", environmentAssignments), - toRepeatedOption("--volume", volumeBindSpecs), - toRepeatedOption("--cap-add", addCapabilitiesList), - toRepeatedOption("--cap-drop", dropCapabilitiesList), - toRepeatedOption("--security-opt", securityOpts), - toRepeatedOption("--dns-option", dnsOptions), - toOptionalOption("--net", networkMode), - toOptionalOption("--ip", ipv4Address), - toOptionalOption("--ip6", ipv6Address), - toOptionalOption("--entrypoint", entrypointExecuteable), - toFlagOption("--privileged", privileged), - dockerImage.asString(), - entrypointArgs) - .filter(s -> !s.isEmpty()) - .collect(Collectors.joining(" ")); - } -} diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/DockerEngine.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/DockerEngine.java deleted file mode 100644 index 3b7b2b8d54c..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/DockerEngine.java +++ /dev/null @@ -1,479 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.dockerjava.api.DockerClient; -import com.github.dockerjava.api.command.ExecCreateCmdResponse; -import com.github.dockerjava.api.command.InspectContainerResponse; -import com.github.dockerjava.api.command.InspectExecResponse; -import com.github.dockerjava.api.command.InspectImageResponse; -import com.github.dockerjava.api.command.PullImageCmd; -import com.github.dockerjava.api.command.UpdateContainerCmd; -import com.github.dockerjava.api.exception.DockerClientException; -import com.github.dockerjava.api.exception.NotFoundException; -import com.github.dockerjava.api.exception.NotModifiedException; -import com.github.dockerjava.api.model.AuthConfig; -import com.github.dockerjava.api.model.HostConfig; -import com.github.dockerjava.api.model.Image; -import com.github.dockerjava.api.model.Statistics; -import com.github.dockerjava.core.DefaultDockerClientConfig; -import com.github.dockerjava.core.DockerClientConfig; -import com.github.dockerjava.core.DockerClientImpl; -import com.github.dockerjava.core.async.ResultCallbackTemplate; -import com.github.dockerjava.core.command.ExecStartResultCallback; -import com.github.dockerjava.core.command.PullImageResultCallback; -import com.github.dockerjava.jaxrs.JerseyDockerCmdExecFactory; -import com.google.inject.Inject; -import com.yahoo.config.provision.DockerImage; -import com.yahoo.vespa.hosted.dockerapi.exception.ContainerNotFoundException; -import com.yahoo.vespa.hosted.dockerapi.exception.DockerException; -import com.yahoo.vespa.hosted.dockerapi.exception.DockerExecTimeoutException; -import com.yahoo.vespa.hosted.dockerapi.metrics.Counter; -import com.yahoo.vespa.hosted.dockerapi.metrics.Dimensions; -import com.yahoo.vespa.hosted.dockerapi.metrics.Gauge; -import com.yahoo.vespa.hosted.dockerapi.metrics.Metrics; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.time.Clock; -import java.time.Duration; -import java.time.Instant; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.OptionalLong; -import java.util.Set; -import java.util.TreeMap; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class DockerEngine implements ContainerEngine { - - private static final Logger logger = Logger.getLogger(DockerEngine.class.getName()); - - static final String LABEL_NAME_MANAGEDBY = "com.yahoo.vespa.managedby"; - private static final String FRAMEWORK_CONTAINER_PREFIX = "/"; - private static final Duration WAIT_BEFORE_KILLING = Duration.ofSeconds(10); - - private final Object monitor = new Object(); - private final Set scheduledPulls = new HashSet<>(); - - private final DockerClient dockerClient; - private final DockerImageGarbageCollector dockerImageGC; - private final Metrics metrics; - private final Counter numberOfDockerApiFails; - private final Clock clock; - - @Inject - public DockerEngine(Metrics metrics) { - this(createDockerClient(), metrics, Clock.systemUTC()); - } - - DockerEngine(DockerClient dockerClient, Metrics metrics, Clock clock) { - this.dockerClient = dockerClient; - this.dockerImageGC = new DockerImageGarbageCollector(this); - this.metrics = metrics; - this.clock = clock; - - numberOfDockerApiFails = metrics.declareCounter("docker.api_fails"); - } - - @Override - public boolean pullImageAsyncIfNeeded(DockerImage image, RegistryCredentials registryCredentials) { - try { - synchronized (monitor) { - if (scheduledPulls.contains(image)) return true; - if (imageIsDownloaded(image)) return false; - - scheduledPulls.add(image); - - logger.log(Level.INFO, "Starting download of " + image.asString()); - PullImageCmd pullCmd = dockerClient.pullImageCmd(image.asString()); - if (!registryCredentials.equals(RegistryCredentials.none)) { - logger.log(Level.INFO, "Authenticating with " + registryCredentials.registryAddress()); - AuthConfig authConfig = new AuthConfig().withUsername(registryCredentials.username()) - .withPassword(registryCredentials.password()) - .withRegistryAddress(registryCredentials.registryAddress()); - pullCmd = pullCmd.withAuthConfig(authConfig); - } - pullCmd.exec(new ImagePullCallback(image)); - return true; - } - } catch (RuntimeException e) { - numberOfDockerApiFails.increment(); - throw new DockerException("Failed to pull image '" + image.asString() + "'", e); - } - } - - private void removeScheduledPoll(DockerImage image) { - synchronized (monitor) { - scheduledPulls.remove(image); - } - } - - /** - * Check if a given image is already in the local registry - */ - boolean imageIsDownloaded(DockerImage dockerImage) { - return inspectImage(dockerImage).isPresent(); - } - - private Optional inspectImage(DockerImage dockerImage) { - try { - return Optional.of(dockerClient.inspectImageCmd(dockerImage.asString()).exec()); - } catch (NotFoundException e) { - return Optional.empty(); - } catch (RuntimeException e) { - numberOfDockerApiFails.increment(); - throw new DockerException("Failed to inspect image '" + dockerImage.asString() + "'", e); - } - } - - @Override - public CreateContainerCommand createContainerCommand(DockerImage image, ContainerName containerName) { - return new CreateContainerCommandImpl(dockerClient, image, containerName); - } - - - @Override - public ProcessResult executeInContainerAsUser(ContainerName containerName, String user, OptionalLong timeoutSeconds, String... command) { - try { - ExecCreateCmdResponse response = execCreateCmd(containerName, user, command); - - ByteArrayOutputStream output = new ByteArrayOutputStream(); - ByteArrayOutputStream errors = new ByteArrayOutputStream(); - ExecStartResultCallback callback = dockerClient.execStartCmd(response.getId()) - .exec(new ExecStartResultCallback(output, errors)); - - if (timeoutSeconds.isPresent()) { - if (!callback.awaitCompletion(timeoutSeconds.getAsLong(), TimeUnit.SECONDS)) - throw new DockerExecTimeoutException(String.format( - "Command '%s' did not finish within %d seconds.", command[0], timeoutSeconds.getAsLong())); - } else { - // Wait for completion no timeout - callback.awaitCompletion(); - } - - InspectExecResponse state = dockerClient.inspectExecCmd(response.getId()).exec(); - if (state.isRunning()) - throw new DockerException("Command '%s' did not finish within %s seconds."); - - return new ProcessResult(state.getExitCode(), new String(output.toByteArray()), new String(errors.toByteArray())); - } catch (RuntimeException | InterruptedException e) { - numberOfDockerApiFails.increment(); - throw new DockerException("Container '" + containerName.asString() - + "' failed to execute " + Arrays.toString(command), e); - } - } - - private ExecCreateCmdResponse execCreateCmd(ContainerName containerName, String user, String... command) { - try { - return dockerClient.execCreateCmd(containerName.asString()) - .withCmd(command) - .withAttachStdout(true) - .withAttachStderr(true) - .withUser(user) - .exec(); - } catch (NotFoundException e) { - throw new ContainerNotFoundException(containerName); - } - } - - private Optional inspectContainerCmd(String container) { - try { - return Optional.of(dockerClient.inspectContainerCmd(container).exec()); - } catch (NotFoundException ignored) { - return Optional.empty(); - } catch (RuntimeException e) { - numberOfDockerApiFails.increment(); - throw new DockerException("Failed to get info for container '" + container + "'", e); - } - } - - @Override - public Optional getContainerStats(ContainerName containerName) { - try { - DockerStatsCallback statsCallback = dockerClient.statsCmd(containerName.asString()).exec(new DockerStatsCallback()); - statsCallback.awaitCompletion(5, TimeUnit.SECONDS); - return statsCallback.stats.map(DockerEngine::containerStatsFrom); - } catch (NotFoundException ignored) { - return Optional.empty(); - } catch (RuntimeException | InterruptedException e) { - numberOfDockerApiFails.increment(); - throw new DockerException("Failed to get stats for container '" + containerName.asString() + "'", e); - } - } - - @Override - public void startContainer(ContainerName containerName) { - try { - dockerClient.startContainerCmd(containerName.asString()).exec(); - } catch (NotFoundException e) { - throw new ContainerNotFoundException(containerName); - } catch (NotModifiedException ignored) { - // If is already started, ignore - } catch (RuntimeException e) { - numberOfDockerApiFails.increment(); - throw new DockerException("Failed to start container '" + containerName.asString() + "'", e); - } - } - - @Override - public void stopContainer(ContainerName containerName) { - try { - dockerClient.stopContainerCmd(containerName.asString()).withTimeout((int) WAIT_BEFORE_KILLING.getSeconds()).exec(); - } catch (NotFoundException e) { - throw new ContainerNotFoundException(containerName); - } catch (NotModifiedException ignored) { - // If is already stopped, ignore - } catch (RuntimeException e) { - numberOfDockerApiFails.increment(); - throw new DockerException("Failed to stop container '" + containerName.asString() + "'", e); - } - } - - @Override - public void deleteContainer(ContainerName containerName) { - try { - dockerClient.removeContainerCmd(containerName.asString()).exec(); - } catch (NotFoundException e) { - throw new ContainerNotFoundException(containerName); - } catch (RuntimeException e) { - numberOfDockerApiFails.increment(); - throw new DockerException("Failed to delete container '" + containerName.asString() + "'", e); - } - } - - @Override - public void updateContainer(ContainerName containerName, ContainerResources resources) { - try { - UpdateContainerCmd updateContainerCmd = dockerClient.updateContainerCmd(containerName.asString()) - .withCpuShares(resources.cpuShares()) - .withMemory(resources.memoryBytes()) - .withMemorySwap(resources.memoryBytes()) - - // Command line argument `--cpus c` is sent over to docker daemon as "NanoCPUs", which is the - // value of `c * 1e9`. This however, is just a shorthand for `--cpu-period p` and `--cpu-quota q` - // where p = 100000 and q = c * 100000. - // See: https://docs.docker.com/config/containers/resource_constraints/#configure-the-default-cfs-scheduler - // --cpus requires API 1.25+ on create and 1.29+ on update - // NanoCPUs is supported in docker-java as of 3.1.0 on create and not at all on update - // TODO: Simplify this to .withNanoCPUs(resources.cpu()) when docker-java supports it - .withCpuPeriod(resources.cpuPeriod()) - .withCpuQuota(resources.cpuQuota()); - - updateContainerCmd.exec(); - } catch (NotFoundException e) { - throw new ContainerNotFoundException(containerName); - } catch (RuntimeException e) { - numberOfDockerApiFails.increment(); - throw new DockerException("Failed to update container '" + containerName.asString() + "' to " + resources, e); - } - } - - @Override - public Optional getContainer(ContainerName containerName) { - return asContainer(containerName.asString()).findFirst(); - } - - private Stream asContainer(String container) { - return inspectContainerCmd(container) - .map(response -> new Container( - new ContainerId(response.getId()), - response.getConfig().getHostName(), - DockerImage.fromString(response.getConfig().getImage()), - containerResourcesFromHostConfig(response.getHostConfig()), - toContainerName(response.getName()), - Container.State.valueOf(response.getState().getStatus().toUpperCase()), - response.getState().getPid() - )) - .stream(); - } - - private static ContainerResources containerResourcesFromHostConfig(HostConfig hostConfig) { - // Docker keeps an internal state of what the period and quota are: in cgroups, the quota is always set - // (default is 100000), but docker will report it as 0 unless explicitly set by the user. - // This may lead to a state where the quota is set, but period is 0 (accord to docker), which will - // mess up the calculation below. This can only happen if someone sets it manually, since this class - // will always set both quota and period. - final double cpus = hostConfig.getCpuQuota() > 0 ? - (double) hostConfig.getCpuQuota() / hostConfig.getCpuPeriod() : 0; - return new ContainerResources(cpus, hostConfig.getCpuShares(), hostConfig.getMemory()); - } - - private boolean isManagedBy(com.github.dockerjava.api.model.Container container, String manager) { - final Map labels = container.getLabels(); - return labels != null && manager.equals(labels.get(LABEL_NAME_MANAGEDBY)); - } - - private ContainerName toContainerName(String encodedContainerName) { - return new ContainerName(encodedContainerName.substring(FRAMEWORK_CONTAINER_PREFIX.length())); - } - - @Override - public boolean noManagedContainersRunning(String manager) { - return listAllContainers().stream() - .filter(container -> isManagedBy(container, manager)) - .noneMatch(container -> "running".equalsIgnoreCase(container.getState())); - } - - @Override - public List listManagedContainers(String manager) { - return listAllContainers().stream() - .filter(container -> isManagedBy(container, manager)) - .map(container -> toContainerName(container.getNames()[0])) - .collect(Collectors.toList()); - } - - List listAllContainers() { - try { - return dockerClient.listContainersCmd().withShowAll(true).exec(); - } catch (RuntimeException e) { - numberOfDockerApiFails.increment(); - throw new DockerException("Failed to list all containers", e); - } - } - - List listAllImages() { - try { - return dockerClient.listImagesCmd().withShowAll(true).exec(); - } catch (RuntimeException e) { - numberOfDockerApiFails.increment(); - throw new DockerException("Failed to list all images", e); - } - } - - void deleteImage(String imageReference) { - try { - dockerClient.removeImageCmd(imageReference).exec(); - } catch (NotFoundException ignored) { - // Image was already deleted, ignore - } catch (RuntimeException e) { - numberOfDockerApiFails.increment(); - throw new DockerException("Failed to delete image by reference '" + imageReference + "'", e); - } - } - - @Override - public boolean deleteUnusedDockerImages(List excludes, Duration minImageAgeToDelete) { - List excludedRefs = excludes.stream().map(DockerImage::asString).collect(Collectors.toList()); - return dockerImageGC.deleteUnusedDockerImages(excludedRefs, minImageAgeToDelete); - } - - private class ImagePullCallback extends PullImageResultCallback { - - private final DockerImage dockerImage; - private final Instant startedAt; - - private ImagePullCallback(DockerImage dockerImage) { - this.dockerImage = dockerImage; - this.startedAt = clock.instant(); - } - - @Override - public void onError(Throwable throwable) { - removeScheduledPoll(dockerImage); - logger.log(Level.SEVERE, "Could not download image " + dockerImage.asString(), throwable); - } - - @Override - public void onComplete() { - if (imageIsDownloaded(dockerImage)) { - logger.log(Level.INFO, "Download completed: " + dockerImage.asString()); - removeScheduledPoll(dockerImage); - } else { - numberOfDockerApiFails.increment(); - throw new DockerClientException("Could not download image: " + dockerImage); - } - sampleDuration(); - } - - private void sampleDuration() { - Gauge gauge = metrics.declareGauge("docker.imagePullDurationSecs", - new Dimensions(Map.of("image", dockerImage.asString()))); - Duration pullDuration = Duration.between(startedAt, clock.instant()); - gauge.sample(pullDuration.getSeconds()); - } - - } - - // docker-java currently (3.0.8) does not support getting docker stats with stream=false, therefore we need - // to subscribe to the stream and complete as soon we get the first result. - private class DockerStatsCallback extends ResultCallbackTemplate { - private Optional stats = Optional.empty(); - private final CountDownLatch completed = new CountDownLatch(1); - - @Override - public void onNext(Statistics stats) { - if (stats != null) { - this.stats = Optional.of(stats); - completed.countDown(); - onComplete(); - } - } - - @Override - public boolean awaitCompletion(long timeout, TimeUnit timeUnit) throws InterruptedException { - // For some reason it takes as long to execute onComplete as the awaitCompletion timeout is, therefore - // we have own awaitCompletion that completes as soon as we get the first result. - return completed.await(timeout, timeUnit); - } - } - - private static DockerClient createDockerClient() { - JerseyDockerCmdExecFactory dockerFactory = new JerseyDockerCmdExecFactory() - .withMaxPerRouteConnections(10) - .withMaxTotalConnections(100) - .withConnectTimeout((int) Duration.ofSeconds(100).toMillis()) - .withReadTimeout((int) Duration.ofMinutes(30).toMillis()); - - DockerClientConfig dockerClientConfig = new DefaultDockerClientConfig.Builder() - .withDockerHost("unix:///var/run/docker.sock") - .build(); - - return DockerClientImpl.getInstance(dockerClientConfig) - .withDockerCmdExecFactory(dockerFactory); - } - - private static ContainerStats containerStatsFrom(Statistics statistics) { - return new ContainerStats(Optional.ofNullable(statistics.getNetworks()).orElseGet(Map::of) - .entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - e -> new ContainerStats.NetworkStats(e.getValue().getRxBytes(), e.getValue().getRxDropped(), - e.getValue().getRxErrors(), e.getValue().getTxBytes(), - e.getValue().getTxDropped(), e.getValue().getTxErrors()), - (u, v) -> { - throw new IllegalStateException(); - }, - TreeMap::new)), - new ContainerStats.MemoryStats(statistics.getMemoryStats().getStats().getCache(), - statistics.getMemoryStats().getUsage(), - statistics.getMemoryStats().getLimit()), - new ContainerStats.CpuStats(statistics.getCpuStats().getCpuUsage().getPercpuUsage().size(), - statistics.getCpuStats().getSystemCpuUsage(), - statistics.getCpuStats().getCpuUsage().getTotalUsage(), - statistics.getCpuStats().getCpuUsage().getUsageInKernelmode(), - statistics.getCpuStats().getThrottlingData().getThrottledTime(), - statistics.getCpuStats().getThrottlingData().getPeriods(), - statistics.getCpuStats().getThrottlingData().getThrottledPeriods())); - } - - // For testing only, create ContainerStats from JSON returned by docker daemon stats API - public static ContainerStats statsFromJson(String json) { - try { - Statistics statistics = new ObjectMapper().readValue(json, Statistics.class); - return containerStatsFrom(statistics); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - -} diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/DockerImageGarbageCollector.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/DockerImageGarbageCollector.java deleted file mode 100644 index 0560d84577a..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/DockerImageGarbageCollector.java +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi; - -import com.github.dockerjava.api.model.Container; -import com.github.dockerjava.api.model.Image; -import com.google.common.base.Strings; -import com.yahoo.collections.Pair; - -import java.time.Clock; -import java.time.Duration; -import java.time.Instant; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * This class keeps track of downloaded docker images and helps delete images that have not been recently used - * - *

Definitions: - *

- * - *

Limitations: - *

    - *
  1. Image that has more than 1 tag cannot be deleted by ID
  2. - *
  3. Deleting a tag of an image with multiple tags will only remove the tag, the image with the - * remaining tags will remain
  4. - *
  5. Deleting the last tag of an image will delete the entire image.
  6. - *
  7. Image cannot be deleted if:
  8. - *
      - *
    1. It has 1 or more children
    2. - *
    3. A container uses it
    4. - *
    - *
- * - * @author freva - */ -class DockerImageGarbageCollector { - private static final Logger logger = Logger.getLogger(DockerImageGarbageCollector.class.getName()); - - private final Map lastTimeUsedByImageId = new ConcurrentHashMap<>(); - private final DockerEngine docker; - private final Clock clock; - - DockerImageGarbageCollector(DockerEngine docker) { - this(docker, Clock.systemUTC()); - } - - DockerImageGarbageCollector(DockerEngine docker, Clock clock) { - this.docker = docker; - this.clock = clock; - } - - /** - * This method must be called frequently enough to see all containers to know which images are being used - * - * @param excludes List of image references (tag or id) that should not be deleted regardless of their used status - * @param minImageAgeToDelete Minimum duration after which an image can be removed if it has not been used - * @return true iff at least 1 image was deleted - */ - boolean deleteUnusedDockerImages(List excludes, Duration minImageAgeToDelete) { - List images = docker.listAllImages(); - List containers = docker.listAllContainers(); - - Map imageByImageId = images.stream().collect(Collectors.toMap(Image::getId, Function.identity())); - - // Find all the ancestors for every local image id, this includes the image id itself - Map> ancestorsByImageId = images.stream() - .map(Image::getId) - .collect(Collectors.toMap( - Function.identity(), - imageId -> { - Set ancestors = new HashSet<>(); - while (!Strings.isNullOrEmpty(imageId)) { - ancestors.add(imageId); - imageId = Optional.of(imageId).map(imageByImageId::get).map(Image::getParentId).orElse(null); - } - return ancestors; - } - )); - - // The set of images that we want to keep is: - // 1. The images that were recently used - // 2. The images that were explicitly excluded - // 3. All of the ancestors of from images in 1 & 2 - Set imagesToKeep = Stream - .concat( - getRecentlyUsedImageIds(images, containers, minImageAgeToDelete).stream(), // 1 - referencesToImages(excludes, images).stream()) // 2 - .flatMap(imageId -> ancestorsByImageId.getOrDefault(imageId, Collections.emptySet()).stream()) // 3 - .collect(Collectors.toSet()); - - // Now take all the images we have locally - return imageByImageId.keySet().stream() - - // filter out images we want to keep - .filter(imageId -> !imagesToKeep.contains(imageId)) - - // Sort images in an order is safe to delete (children before parents) - .sorted((o1, o2) -> { - // If image2 is parent of image1, image1 comes before image2 - if (imageIsDescendantOf(imageByImageId, o1, o2)) return -1; - // If image1 is parent of image2, image2 comes before image1 - else if (imageIsDescendantOf(imageByImageId, o2, o1)) return 1; - // Otherwise, sort lexicographically by image name (For testing) - else return o1.compareTo(o2); - }) - - // Map back to image - .map(imageByImageId::get) - - // Delete image, if successful also remove last usage time to prevent re-download being instantly deleted - .peek(image -> { - // Deleting an image by image ID with multiple tags will fail -> delete by tags instead - referencesOf(image).forEach(imageReference -> { - logger.info("Deleting unused image " + imageReference); - docker.deleteImage(imageReference); - }); - lastTimeUsedByImageId.remove(image.getId()); - }) - .count() > 0; - } - - private Set getRecentlyUsedImageIds(List images, List containers, Duration minImageAgeToDelete) { - final Instant now = clock.instant(); - - // Add any already downloaded image to the list once - images.forEach(image -> lastTimeUsedByImageId.putIfAbsent(image.getId(), now)); - - // Update last used time for all current containers - containers.forEach(container -> lastTimeUsedByImageId.put(container.getImageId(), now)); - - // Return list of images that have been used within minImageAgeToDelete - return lastTimeUsedByImageId.entrySet().stream() - .filter(entry -> Duration.between(entry.getValue(), now).minus(minImageAgeToDelete).isNegative()) - .map(Map.Entry::getKey) - .collect(Collectors.toSet()); - } - - /** - * Map given references (image tags or ids) to images. - * - * This only works if the given tag is actually present locally. This is fine, because if it isn't - we can't delete - * it, so no harm done. - */ - private Set referencesToImages(List references, List images) { - Map imageIdByImageTag = images.stream() - .flatMap(image -> referencesOf(image).stream() - .map(repoTag -> new Pair<>(repoTag, image.getId()))) - .collect(Collectors.toMap(Pair::getFirst, Pair::getSecond)); - - return references.stream() - .map(ref -> imageIdByImageTag.getOrDefault(ref, ref)) - .collect(Collectors.toUnmodifiableSet()); - } - - /** - * @return true if ancestor is a parent or grand-parent or grand-grand-parent, etc. of img - */ - private boolean imageIsDescendantOf(Map imageIdToImage, String img, String ancestor) { - while (imageIdToImage.containsKey(img)) { - img = imageIdToImage.get(img).getParentId(); - if (img == null) return false; - if (ancestor.equals(img)) return true; - } - return false; - } - - /** - * Returns list of references to given image, preferring image tag(s), if any exist. - * - * If image is untagged, its ID is returned instead. - */ - private static List referencesOf(Image image) { - if (image.getRepoTags() == null) { - return List.of(image.getId()); - } - return Arrays.stream(image.getRepoTags()) - // Docker API returns untagged images as having the tag ":". - .map(tag -> { - if (":".equals(tag)) return image.getId(); - return tag; - }) - .collect(Collectors.toUnmodifiableList()); - } - -} diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ProcessResult.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ProcessResult.java deleted file mode 100644 index eb81b40434a..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/ProcessResult.java +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi; - -import java.util.Objects; - -// TODO: Consider replacing usages of this with CommandResult when docker-api module can be removed -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/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/RegistryCredentials.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/RegistryCredentials.java deleted file mode 100644 index 130519ecfd6..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/RegistryCredentials.java +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi; - -import java.util.Objects; - -/** - * Credentials for a container registry server. - * - * @author mpolden - */ -// TODO: Move this to node-admin when docker-api module can be removed -public class RegistryCredentials { - - public static final RegistryCredentials none = new RegistryCredentials("", "", ""); - - private final String username; - private final String password; - private final String registryAddress; - - public RegistryCredentials(String username, String password, String registryAddress) { - this.username = Objects.requireNonNull(username); - this.password = Objects.requireNonNull(password); - this.registryAddress = Objects.requireNonNull(registryAddress); - } - - public String username() { - return username; - } - - public String password() { - return password; - } - - public String registryAddress() { - return registryAddress; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - RegistryCredentials that = (RegistryCredentials) o; - return username.equals(that.username) && - password.equals(that.password) && - registryAddress.equals(that.registryAddress); - } - - @Override - public int hashCode() { - return Objects.hash(username, password, registryAddress); - } - - @Override - public String toString() { - return "registry credentials for " + registryAddress + " [username=" + username + ",password=" + password + "]"; - } - -} diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/exception/ContainerNotFoundException.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/exception/ContainerNotFoundException.java deleted file mode 100644 index b237228ee8e..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/exception/ContainerNotFoundException.java +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi.exception; - -import com.yahoo.vespa.hosted.dockerapi.ContainerName; - -/** - * @author freva - */ -public class ContainerNotFoundException extends DockerException { - public ContainerNotFoundException(ContainerName containerName) { - super("No such container: " + containerName.asString()); - } -} diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/exception/DockerException.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/exception/DockerException.java deleted file mode 100644 index df6bb702bf7..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/exception/DockerException.java +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi.exception; - -/** - * This exception wraps any exception thrown by docker-java - */ -@SuppressWarnings("serial") -public class DockerException extends RuntimeException { - public DockerException(String message) { - super(message); - } - - public DockerException(String message, Exception exception) { - super(message, exception); - } -} diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/exception/DockerExecTimeoutException.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/exception/DockerExecTimeoutException.java deleted file mode 100644 index 39813db5c1e..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/exception/DockerExecTimeoutException.java +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi.exception; - -/** - * Runtime exception to be thrown when the exec commands did not finish in time. - * - * The underlying process has not been killed. If you need the process to be - * killed you need to wrap it into a commands that times out. - * - * @author smorgrav - */ -@SuppressWarnings("serial") -public class DockerExecTimeoutException extends DockerException { - public DockerExecTimeoutException(String msg) { - super(msg); - } -} diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/exception/package-info.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/exception/package-info.java deleted file mode 100644 index a5ec5f6c235..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/exception/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -@ExportPackage -package com.yahoo.vespa.hosted.dockerapi.exception; - -import com.yahoo.osgi.annotation.ExportPackage; diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/Counter.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/Counter.java deleted file mode 100644 index 3a0b820c846..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/Counter.java +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi.metrics; - -/** - * @author freva - */ -public class Counter implements MetricValue { - private final Object lock = new Object(); - - private long value = 0; - - public void increment() { - add(1L); - } - - public void add(long n) { - synchronized (lock) { - value += n; - } - } - - @Override - public Number getValue() { - synchronized (lock) { - return value; - } - } -} diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/DimensionMetrics.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/DimensionMetrics.java deleted file mode 100644 index 590ef207e3f..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/DimensionMetrics.java +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi.metrics; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; - -/** - * @author freva - */ -public class DimensionMetrics { - - private final String application; - private final Dimensions dimensions; - private final Map metrics; - - DimensionMetrics(String application, Dimensions dimensions, Map metrics) { - this.application = Objects.requireNonNull(application); - this.dimensions = Objects.requireNonNull(dimensions); - this.metrics = metrics.entrySet().stream() - .filter(DimensionMetrics::metricIsFinite) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - } - - public String getApplication() { - return application; - } - - public Dimensions getDimensions() { - return dimensions; - } - - public Map getMetrics() { - return metrics; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - DimensionMetrics that = (DimensionMetrics) o; - return application.equals(that.application) && - dimensions.equals(that.dimensions) && - metrics.equals(that.metrics); - } - - @Override - public int hashCode() { - return Objects.hash(application, dimensions, metrics); - } - - private static boolean metricIsFinite(Map.Entry metric) { - return ! (metric.getValue() instanceof Double) || Double.isFinite((double) metric.getValue()); - } - - public static class Builder { - private final String application; - private final Dimensions dimensions; - private final Map metrics = new HashMap<>(); - - public Builder(String application, Dimensions dimensions) { - this.application = application; - this.dimensions = dimensions; - } - - public Builder withMetric(String metricName, Number metricValue) { - metrics.put(metricName, metricValue); - return this; - } - - public DimensionMetrics build() { - return new DimensionMetrics(application, dimensions, metrics); - } - } -} diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/Dimensions.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/Dimensions.java deleted file mode 100644 index 63b92e06505..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/Dimensions.java +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi.metrics; - -import java.util.HashMap; -import java.util.Map; - -/** - * @author freva - */ -public class Dimensions { - - public static final Dimensions NONE = new Dimensions(Map.of()); - - private final Map dimensionsMap; - - public Dimensions(Map dimensionsMap) { - this.dimensionsMap = Map.copyOf(dimensionsMap); - } - - public Map asMap() { - return dimensionsMap; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Dimensions that = (Dimensions) o; - return dimensionsMap.equals(that.dimensionsMap); - } - - @Override - public int hashCode() { - return dimensionsMap.hashCode(); - } - - @Override - public String toString() { - return dimensionsMap.toString(); - } - - public static class Builder { - private final Map dimensionsMap = new HashMap<>(); - - public Dimensions.Builder add(String dimensionName, String dimensionValue) { - dimensionsMap.put(dimensionName, dimensionValue); - return this; - } - - public Dimensions build() { - return new Dimensions(dimensionsMap); - } - } -} diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/Gauge.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/Gauge.java deleted file mode 100644 index b413475fc2b..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/Gauge.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi.metrics; - -/** - * @author freva - */ -public class Gauge implements MetricValue { - private final Object lock = new Object(); - - private double value; - - public void sample(double x) { - synchronized (lock) { - this.value = x; - } - } - - @Override - public Number getValue() { - synchronized (lock) { - return value; - } - } -} diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/MetricValue.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/MetricValue.java deleted file mode 100644 index b20aa1b11ff..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/MetricValue.java +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi.metrics; - -/** - * @author freva - */ -public interface MetricValue { - Number getValue(); -} diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/Metrics.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/Metrics.java deleted file mode 100644 index f9b169f0a93..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/Metrics.java +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi.metrics; - -import com.google.inject.Inject; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; - -/** - * Stores the latest metric for the given application, name, dimension triplet in memory - * - * @author freva - */ -public class Metrics { - // Application names used - public static final String APPLICATION_HOST = "vespa.host"; - public static final String APPLICATION_NODE = "vespa.node"; - - private final Object monitor = new Object(); - private final Map> metrics = new HashMap<>(); - - @Inject - public Metrics() { } - - /** - * Creates a counter metric under vespa.host application, with no dimensions and default dimension type - * See {@link #declareCounter(String, String, Dimensions, DimensionType)} - */ - public Counter declareCounter(String name) { - return declareCounter(name, Dimensions.NONE); - } - - /** - * Creates a counter metric under vespa.host application, with the given dimensions and default dimension type - * See {@link #declareCounter(String, String, Dimensions, DimensionType)} - */ - public Counter declareCounter(String name, Dimensions dimensions) { - return declareCounter(APPLICATION_HOST, name, dimensions, DimensionType.DEFAULT); - } - - /** Creates a counter metric. This method is idempotent. */ - public Counter declareCounter(String application, String name, Dimensions dimensions, DimensionType type) { - synchronized (monitor) { - return (Counter) getOrCreateApplicationMetrics(application, type) - .computeIfAbsent(dimensions, d -> new HashMap<>()) - .computeIfAbsent(name, n -> new Counter()); - } - } - - /** - * Creates a gauge metric under vespa.host application, with no dimensions and default dimension type - * See {@link #declareGauge(String, String, Dimensions, DimensionType)} - */ - public Gauge declareGauge(String name) { - return declareGauge(name, Dimensions.NONE); - } - - /** - * Creates a gauge metric under vespa.host application, with the given dimensions and default dimension type - * See {@link #declareGauge(String, String, Dimensions, DimensionType)} - */ - public Gauge declareGauge(String name, Dimensions dimensions) { - return declareGauge(APPLICATION_HOST, name, dimensions, DimensionType.DEFAULT); - } - - /** Creates a gauge metric. This method is idempotent */ - public Gauge declareGauge(String application, String name, Dimensions dimensions, DimensionType type) { - synchronized (monitor) { - return (Gauge) getOrCreateApplicationMetrics(application, type) - .computeIfAbsent(dimensions, d -> new HashMap<>()) - .computeIfAbsent(name, n -> new Gauge()); - } - } - - public List getDefaultMetrics() { - return getMetricsByType(DimensionType.DEFAULT); - } - - public List getMetricsByType(DimensionType type) { - synchronized (monitor) { - List dimensionMetrics = new ArrayList<>(); - metrics.getOrDefault(type, Map.of()) - .forEach((application, applicationMetrics) -> applicationMetrics.metricsByDimensions().entrySet().stream() - .map(entry -> new DimensionMetrics(application, entry.getKey(), - entry.getValue().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, value -> value.getValue().getValue())))) - .forEach(dimensionMetrics::add)); - return dimensionMetrics; - } - } - - public void deleteMetricByDimension(String name, Dimensions dimensionsToRemove, DimensionType type) { - synchronized (monitor) { - Optional.ofNullable(metrics.get(type)) - .map(m -> m.get(name)) - .map(ApplicationMetrics::metricsByDimensions) - .ifPresent(m -> m.remove(dimensionsToRemove)); - } - } - - Map> getOrCreateApplicationMetrics(String application, DimensionType type) { - return metrics.computeIfAbsent(type, m -> new HashMap<>()) - .computeIfAbsent(application, app -> new ApplicationMetrics()) - .metricsByDimensions(); - } - - // "Application" is the monitoring application, not Vespa application - private static class ApplicationMetrics { - private final Map> metricsByDimensions = new LinkedHashMap<>(); - - Map> metricsByDimensions() { - return metricsByDimensions; - } - } - - // Used to distinguish whether metrics have been populated with all tag vaules - public enum DimensionType { - /** Default metrics get added default dimensions set in check config */ - DEFAULT, - - /** Pretagged metrics will only get the dimensions explicitly set when creating the counter/gauge */ - PRETAGGED - } -} diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/package-info.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/package-info.java deleted file mode 100644 index 3e511560d94..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/metrics/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -@ExportPackage -package com.yahoo.vespa.hosted.dockerapi.metrics; - -import com.yahoo.osgi.annotation.ExportPackage; diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/package-info.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/package-info.java deleted file mode 100644 index 2d34d85876d..00000000000 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -@ExportPackage -package com.yahoo.vespa.hosted.dockerapi; - -import com.yahoo.osgi.annotation.ExportPackage; diff --git a/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/ContainerNameTest.java b/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/ContainerNameTest.java deleted file mode 100644 index cace96ab207..00000000000 --- a/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/ContainerNameTest.java +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -/** - * @author freva - */ -public class ContainerNameTest { - @Test - public void testAlphanumericalContainerName() { - String name = "container123"; - ContainerName containerName = new ContainerName(name); - assertEquals(containerName.asString(), name); - } - - @Test - public void testAlphanumericalWithDashContainerName() { - String name = "container-123"; - ContainerName containerName = new ContainerName(name); - assertEquals(containerName.asString(), name); - } - - @Test - public void testContainerNameFromHostname() { - assertEquals(new ContainerName("container-123"), ContainerName.fromHostname("container-123.sub.domain.tld")); - } - - @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/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/ContainerResourcesTest.java b/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/ContainerResourcesTest.java deleted file mode 100644 index daf4639ad29..00000000000 --- a/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/ContainerResourcesTest.java +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -/** - * @author freva - */ -public class ContainerResourcesTest { - - @Test - public void verify_unlimited() { - assertEquals(-1, ContainerResources.UNLIMITED.cpuQuota()); - assertEquals(100_000, ContainerResources.UNLIMITED.cpuPeriod()); - assertEquals(0, ContainerResources.UNLIMITED.cpuShares()); - } - - @Test - public void validate_shares() { - new ContainerResources(0, 0, 0); - new ContainerResources(0, 2, 0); - new ContainerResources(0, 2048, 0); - new ContainerResources(0, 262_144, 0); - - assertThrows(IllegalArgumentException.class, () -> new ContainerResources(0, -1, 0)); // Negative shares not allowed - assertThrows(IllegalArgumentException.class, () -> new ContainerResources(0, 1, 0)); // 1 share not allowed - assertThrows(IllegalArgumentException.class, () -> new ContainerResources(0, 262_145, 0)); - } - - @Test - public void cpu_shares_scaling() { - ContainerResources resources = ContainerResources.from(5.3, 2.5, 0); - assertEquals(530_000, resources.cpuQuota()); - assertEquals(100_000, resources.cpuPeriod()); - assertEquals(80, resources.cpuShares()); - } - - private static void assertThrows(Class clazz, Runnable runnable) { - try { - runnable.run(); - fail("Expected " + clazz); - } catch (Throwable e) { - if (!clazz.isInstance(e)) throw e; - } - } -} diff --git a/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/CreateContainerCommandImplTest.java b/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/CreateContainerCommandImplTest.java deleted file mode 100644 index b4ba6dbb502..00000000000 --- a/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/CreateContainerCommandImplTest.java +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi; - -import com.yahoo.config.provision.DockerImage; -import org.junit.Test; - -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.nio.file.Paths; - -import static org.junit.Assert.assertEquals; - -public class CreateContainerCommandImplTest { - - @Test - public void testToString() throws UnknownHostException { - DockerImage dockerImage = DockerImage.fromString("docker.registry.domain.tld/my/image:1.2.3"); - ContainerResources containerResources = new ContainerResources(2.5, 100, 1024); - String hostname = "docker-1.region.domain.tld"; - ContainerName containerName = ContainerName.fromHostname(hostname); - - ContainerEngine.CreateContainerCommand createContainerCommand = new CreateContainerCommandImpl( - null, dockerImage, containerName) - .withHostName(hostname) - .withResources(containerResources) - .withLabel("my-label", "test-label") - .withUlimit("nofile", 1, 2) - .withUlimit("nproc", 10, 20) - .withEnvironment("env1", "val1") - .withEnvironment("env2", "val2") - .withVolume(Paths.get("vol1"), Paths.get("/host/vol1")) - .withAddCapability("SYS_PTRACE") - .withAddCapability("SYS_ADMIN") - .withDropCapability("NET_ADMIN") - .withNetworkMode("bridge") - .withIpAddress(InetAddress.getByName("10.0.0.1")) - .withIpAddress(InetAddress.getByName("::1")) - .withEntrypoint("/path/to/program", "arg1", "arg2") - .withPrivileged(true); - - assertEquals("--name docker-1 " + - "--hostname docker-1.region.domain.tld " + - "--cpu-shares 100 " + - "--cpus 2.5 " + - "--memory 1024 " + - "--label my-label=test-label " + - "--ulimit nofile=1:2 " + - "--ulimit nproc=10:20 " + - "--pids-limit -1 " + - "--env env1=val1 " + - "--env env2=val2 " + - "--volume vol1:/host/vol1:Z " + - "--cap-add SYS_ADMIN " + - "--cap-add SYS_PTRACE " + - "--cap-drop NET_ADMIN " + - "--net bridge " + - "--ip 10.0.0.1 " + - "--ip6 0:0:0:0:0:0:0:1 " + - "--entrypoint /path/to/program " + - "--privileged docker.registry.domain.tld/my/image:1.2.3 " + - "arg1 " + - "arg2", createContainerCommand.toString()); - } -} diff --git a/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/DockerEngineTest.java b/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/DockerEngineTest.java deleted file mode 100644 index 71bdb321305..00000000000 --- a/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/DockerEngineTest.java +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi; - -import com.github.dockerjava.api.DockerClient; -import com.github.dockerjava.api.async.ResultCallback; -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.InspectImageCmd; -import com.github.dockerjava.api.command.InspectImageResponse; -import com.github.dockerjava.api.command.PullImageCmd; -import com.github.dockerjava.api.exception.NotFoundException; -import com.github.dockerjava.core.command.ExecStartResultCallback; -import com.yahoo.config.provision.DockerImage; -import com.yahoo.test.ManualClock; -import com.yahoo.vespa.hosted.dockerapi.metrics.DimensionMetrics; -import com.yahoo.vespa.hosted.dockerapi.metrics.Metrics; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; - -import java.time.Duration; -import java.util.Optional; -import java.util.OptionalLong; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * @author Tony Vaagenes - */ -public class DockerEngineTest { - - private final DockerClient dockerClient = mock(DockerClient.class); - private final Metrics metrics = new Metrics(); - private final ManualClock clock = new ManualClock(); - private final DockerEngine docker = new DockerEngine(dockerClient, metrics, clock); - - @Test - public void testExecuteCompletes() { - final String containerId = "container-id"; - final String[] command = new String[] {"/bin/ls", "-l"}; - final String execId = "exec-id"; - final int exitCode = 3; - - 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(ArgumentMatchers.any())).thenReturn(execCreateCmd); - when(execCreateCmd.withAttachStdout(any(Boolean.class))).thenReturn(execCreateCmd); - when(execCreateCmd.withAttachStderr(any(Boolean.class))).thenReturn(execCreateCmd); - when(execCreateCmd.withUser(any(String.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 ProcessResult result = docker.executeInContainerAsUser( - new ContainerName(containerId), "root", OptionalLong.empty(), command); - assertEquals(exitCode, result.getExitStatus()); - } - - @Test - @SuppressWarnings({"unchecked", "rawtypes"}) - public void pullImageAsyncIfNeededSuccessfully() { - final DockerImage image = DockerImage.fromString("registry.example.com/test:1.2.3"); - - InspectImageResponse inspectImageResponse = mock(InspectImageResponse.class); - when(inspectImageResponse.getId()).thenReturn(image.asString()); - - InspectImageCmd imageInspectCmd = mock(InspectImageCmd.class); - when(imageInspectCmd.exec()) - .thenThrow(new NotFoundException("Image not found")) - .thenReturn(inspectImageResponse); - - ArgumentCaptor resultCallback = ArgumentCaptor.forClass(ResultCallback.class); - PullImageCmd pullImageCmd = mock(PullImageCmd.class); - when(pullImageCmd.exec(resultCallback.capture())).thenReturn(null); - - when(dockerClient.inspectImageCmd(image.asString())).thenReturn(imageInspectCmd); - when(dockerClient.pullImageCmd(eq(image.asString()))).thenReturn(pullImageCmd); - - assertTrue("Should return true, we just scheduled the pull", docker.pullImageAsyncIfNeeded(image, RegistryCredentials.none)); - assertTrue("Should return true, the pull i still ongoing", docker.pullImageAsyncIfNeeded(image, RegistryCredentials.none)); - - assertTrue(docker.imageIsDownloaded(image)); - clock.advance(Duration.ofMinutes(10)); - resultCallback.getValue().onComplete(); - assertPullDuration(Duration.ofMinutes(10), image.asString()); - assertFalse(docker.pullImageAsyncIfNeeded(image, RegistryCredentials.none)); - } - - @Test - @SuppressWarnings({"unchecked", "rawtypes"}) - public void pullImageAsyncIfNeededWithError() { - final DockerImage image = DockerImage.fromString("registry.example.com/test:1.2.3"); - - InspectImageCmd imageInspectCmd = mock(InspectImageCmd.class); - when(imageInspectCmd.exec()).thenThrow(new NotFoundException("Image not found")); - - ArgumentCaptor resultCallback = ArgumentCaptor.forClass(ResultCallback.class); - PullImageCmd pullImageCmd = mock(PullImageCmd.class); - when(pullImageCmd.exec(resultCallback.capture())).thenReturn(null); - - when(dockerClient.inspectImageCmd(image.asString())).thenReturn(imageInspectCmd); - when(dockerClient.pullImageCmd(eq(image.asString()))).thenReturn(pullImageCmd); - - assertTrue("Should return true, we just scheduled the pull", docker.pullImageAsyncIfNeeded(image, RegistryCredentials.none)); - assertTrue("Should return true, the pull is still ongoing", docker.pullImageAsyncIfNeeded(image, RegistryCredentials.none)); - - try { - resultCallback.getValue().onComplete(); - } catch (Exception ignored) { } - - assertFalse(docker.imageIsDownloaded(image)); - assertTrue("Should return true, new pull scheduled", docker.pullImageAsyncIfNeeded(image, RegistryCredentials.none)); - } - - private void assertPullDuration(Duration duration, String image) { - Optional byImage = metrics.getDefaultMetrics().stream() - .filter(metrics -> image.equals(metrics.getDimensions().asMap().get("image"))) - .findFirst(); - assertTrue("Found metric for image=" + image, byImage.isPresent()); - Number durationInSecs = byImage.get().getMetrics().get("docker.imagePullDurationSecs"); - assertNotNull(durationInSecs); - assertEquals(duration, Duration.ofSeconds(durationInSecs.longValue())); - } - -} diff --git a/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/DockerImageGarbageCollectionTest.java b/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/DockerImageGarbageCollectionTest.java deleted file mode 100644 index c725b0642c9..00000000000 --- a/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/DockerImageGarbageCollectionTest.java +++ /dev/null @@ -1,284 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.dockerjava.api.model.Image; -import com.yahoo.test.ManualClock; -import org.junit.Test; - -import java.io.IOException; -import java.time.Duration; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * @author freva - */ -public class DockerImageGarbageCollectionTest { - - private final ImageGcTester gcTester = new ImageGcTester(); - - @Test - public void noImagesMeansNoUnusedImages() { - gcTester.withExistingImages() - .expectDeletedImages(); - } - - @Test - public void singleImageWithoutContainersIsUnused() { - gcTester.withExistingImages(new ImageBuilder("image-1")) - // Even though nothing is using the image, we will keep it for at least 1h - .expectDeletedImagesAfterMinutes(0) - .expectDeletedImagesAfterMinutes(30) - .expectDeletedImagesAfterMinutes(30, "image-1"); - } - - @Test - public void singleImageWithContainerIsUsed() { - gcTester.withExistingImages(ImageBuilder.forId("image-1")) - .andExistingContainers(ContainerBuilder.forId("container-1").withImageId("image-1")) - .expectDeletedImages(); - } - - @Test - public void multipleUnusedImagesAreIdentified() { - gcTester.withExistingImages( - ImageBuilder.forId("image-1"), - ImageBuilder.forId("image-2")) - .expectDeletedImages("image-1", "image-2"); - } - - @Test - public void multipleUnusedLeavesAreIdentified() { - gcTester.withExistingImages( - ImageBuilder.forId("parent-image"), - ImageBuilder.forId("image-1").withParentId("parent-image"), - ImageBuilder.forId("image-2").withParentId("parent-image")) - .expectDeletedImages("image-1", "image-2", "parent-image"); - } - - @Test - public void unusedLeafWithUsedSiblingIsIdentified() { - gcTester.withExistingImages( - ImageBuilder.forId("parent-image"), - ImageBuilder.forId("image-1").withParentId("parent-image").withTags("latest"), - ImageBuilder.forId("image-2").withParentId("parent-image").withTags("1.24")) - .andExistingContainers(ContainerBuilder.forId("vespa-node-1").withImageId("image-1")) - .expectDeletedImages("1.24"); // Deleting the only tag will delete the image - } - - @Test - public void unusedImagesWithMultipleTags() { - gcTester.withExistingImages( - ImageBuilder.forId("parent-image"), - ImageBuilder.forId("image-1").withParentId("parent-image") - .withTags("vespa-6", "vespa-6.28", "vespa:latest")) - .expectDeletedImages("vespa-6", "vespa-6.28", "vespa:latest", "parent-image"); - } - - - @Test - public void unusedImagesWithMultipleUntagged() { - gcTester.withExistingImages(ImageBuilder.forId("image1") - .withTags(":"), - ImageBuilder.forId("image2") - .withTags(":")) - .expectDeletedImages("image1", "image2"); - } - - @Test - public void taggedImageWithNoContainersIsUnused() { - gcTester.withExistingImages(ImageBuilder.forId("image-1").withTags("vespa-6")) - .expectDeletedImages("vespa-6"); - } - - @Test - public void unusedImagesWithSimpleImageGc() { - gcTester.withExistingImages(ImageBuilder.forId("parent-image")) - .expectDeletedImagesAfterMinutes(30) - .withExistingImages( - ImageBuilder.forId("parent-image"), - ImageBuilder.forId("image-1").withParentId("parent-image")) - .expectDeletedImagesAfterMinutes(0) - .expectDeletedImagesAfterMinutes(30) - // At this point, parent-image has been unused for 1h, but image-1 depends on parent-image and it has - // only been unused for 30m, so we cannot delete parent-image yet. 30 mins later both can be removed - .expectDeletedImagesAfterMinutes(30, "image-1", "parent-image"); - } - - @Test - public void reDownloadingImageIsNotImmediatelyDeleted() { - gcTester.withExistingImages(ImageBuilder.forId("image")) - .expectDeletedImages("image") // After 1h we delete image - .expectDeletedImagesAfterMinutes(0) // image is immediately re-downloaded, but is not deleted - .expectDeletedImagesAfterMinutes(10) - .expectDeletedImages("image"); // 1h after re-download it is deleted again - } - - @Test - public void reDownloadingImageIsNotImmediatelyDeletedWhenDeletingByTag() { - gcTester.withExistingImages(ImageBuilder.forId("image").withTags("image-1", "my-tag")) - .expectDeletedImages("image-1", "my-tag") // After 1h we delete image - .expectDeletedImagesAfterMinutes(0) // image is immediately re-downloaded, but is not deleted - .expectDeletedImagesAfterMinutes(10) - .expectDeletedImages("image-1", "my-tag"); // 1h after re-download it is deleted again - } - - /** Same scenario as in {@link #multipleUnusedImagesAreIdentified()} */ - @Test - public void doesNotDeleteExcludedByIdImages() { - gcTester.withExistingImages( - ImageBuilder.forId("parent-image"), - ImageBuilder.forId("image-1").withParentId("parent-image"), - ImageBuilder.forId("image-2").withParentId("parent-image")) - // Normally, image-1 and parent-image should also be deleted, but because we exclude image-1 - // we cannot delete parent-image, so only image-2 is deleted - .expectDeletedImages(List.of("image-1"), "image-2"); - } - - /** Same as in {@link #doesNotDeleteExcludedByIdImages()} but with tags */ - @Test - public void doesNotDeleteExcludedByTagImages() { - gcTester.withExistingImages( - ImageBuilder.forId("parent-image").withTags("rhel-6"), - ImageBuilder.forId("image-1").withParentId("parent-image").withTags("vespa:6.288.16"), - ImageBuilder.forId("image-2").withParentId("parent-image").withTags("vespa:6.289.94")) - .expectDeletedImages(List.of("vespa:6.288.16"), "vespa:6.289.94"); - } - - @Test - public void exludingNotDownloadedImageIsNoop() { - gcTester.withExistingImages( - ImageBuilder.forId("parent-image").withTags("rhel-6"), - ImageBuilder.forId("image-1").withParentId("parent-image").withTags("vespa:6.288.16"), - ImageBuilder.forId("image-2").withParentId("parent-image").withTags("vespa:6.289.94")) - .expectDeletedImages(List.of("vespa:6.300.1"), "vespa:6.288.16", "vespa:6.289.94", "rhel-6"); - } - - private class ImageGcTester { - private final DockerEngine docker = mock(DockerEngine.class); - private final ManualClock clock = new ManualClock(); - private final DockerImageGarbageCollector imageGC = new DockerImageGarbageCollector(docker, clock); - private final Map numDeletes = new HashMap<>(); - private boolean initialized = false; - - private ImageGcTester withExistingImages(ImageBuilder... images) { - when(docker.listAllImages()).thenReturn(Arrays.stream(images) - .map(ImageBuilder::toImage) - .collect(Collectors.toList())); - return this; - } - - private ImageGcTester andExistingContainers(ContainerBuilder... containers) { - when(docker.listAllContainers()).thenReturn(Arrays.stream(containers) - .map(ContainerBuilder::toContainer) - .collect(Collectors.toList())); - return this; - } - - private ImageGcTester expectDeletedImages(String... imageIds) { - return expectDeletedImagesAfterMinutes(60, imageIds); - } - - private ImageGcTester expectDeletedImages(List except, String... imageIds) { - return expectDeletedImagesAfterMinutes(60, except, imageIds); - } - private ImageGcTester expectDeletedImagesAfterMinutes(int minutesAfter, String... imageIds) { - return expectDeletedImagesAfterMinutes(minutesAfter, Collections.emptyList(), imageIds); - } - - private ImageGcTester expectDeletedImagesAfterMinutes(int minutesAfter, List except, String... imageIds) { - if (!initialized) { - // Run once with a very long expiry to initialize internal state of existing images - imageGC.deleteUnusedDockerImages(Collections.emptyList(), Duration.ofDays(999)); - initialized = true; - } - - clock.advance(Duration.ofMinutes(minutesAfter)); - - imageGC.deleteUnusedDockerImages(except, Duration.ofHours(1).minusSeconds(1)); - - Arrays.stream(imageIds) - .forEach(imageId -> { - int newValue = numDeletes.getOrDefault(imageId, 0) + 1; - numDeletes.put(imageId, newValue); - verify(docker, times(newValue)).deleteImage(eq(imageId)); - }); - - verify(docker, times(numDeletes.values().stream().mapToInt(i -> i).sum())).deleteImage(any()); - return this; - } - } - - /** - * 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 createFrom(Class 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 = ""; // docker-java returns empty string and not null if the parent is not present - - @JsonProperty("RepoTags") - private String[] repoTags = null; - - private ImageBuilder(String id) { this.id = id; } - - private static ImageBuilder forId(String id) { return new ImageBuilder(id); } - private ImageBuilder withParentId(String parentId) { this.parentId = parentId; return this; } - private ImageBuilder withTags(String... tags) { this.repoTags = tags; return this; } - private 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("ImageID") - private String imageId; - - private ContainerBuilder(String id) { this.id = id; } - private static ContainerBuilder forId(final String id) { return new ContainerBuilder(id); } - private ContainerBuilder withImageId(String imageId) { this.imageId = imageId; return this; } - - private com.github.dockerjava.api.model.Container toContainer() { - return createFrom(com.github.dockerjava.api.model.Container.class, this); - } - } -} diff --git a/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/ProcessResultTest.java b/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/ProcessResultTest.java deleted file mode 100644 index 590833151f2..00000000000 --- a/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/ProcessResultTest.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class ProcessResultTest { - @Test - public void testBasicProperties() { - ProcessResult processResult = new ProcessResult(0, "foo", "bar"); - assertEquals(0, processResult.getExitStatus()); - assertEquals("foo", processResult.getOutput()); - assertTrue(processResult.isSuccess()); - } - - @Test - public void testSuccessFails() { - ProcessResult processResult = new ProcessResult(1, "foo", "bar"); - assertFalse(processResult.isSuccess()); - } -} diff --git a/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/metrics/MetricsTest.java b/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/metrics/MetricsTest.java deleted file mode 100644 index fc153ee0562..00000000000 --- a/docker-api/src/test/java/com/yahoo/vespa/hosted/dockerapi/metrics/MetricsTest.java +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.dockerapi.metrics; - -import org.junit.Test; - -import java.util.Map; -import java.util.stream.Collectors; - -import static com.yahoo.vespa.hosted.dockerapi.metrics.Metrics.APPLICATION_HOST; -import static com.yahoo.vespa.hosted.dockerapi.metrics.Metrics.DimensionType.DEFAULT; -import static org.junit.Assert.assertEquals; - -/** - * @author freva - */ -public class MetricsTest { - private static final Dimensions hostDimension = new Dimensions.Builder().add("host", "abc.yahoo.com").build(); - private final Metrics metrics = new Metrics(); - - @Test - public void testDefaultValue() { - metrics.declareCounter("some.name", hostDimension); - - assertEquals(getMetricsForDimension(hostDimension).get("some.name"), 0L); - } - - @Test - public void testSimpleIncrementMetric() { - Counter counter = metrics.declareCounter("a_counter.value", hostDimension); - - counter.add(5); - counter.add(8); - - Map latestMetrics = getMetricsForDimension(hostDimension); - assertEquals("Expected only 1 metric value to be set", 1, latestMetrics.size()); - assertEquals(latestMetrics.get("a_counter.value"), 13L); // 5 + 8 - } - - @Test - public void testSimpleGauge() { - Gauge gauge = metrics.declareGauge("test.gauge", hostDimension); - - gauge.sample(42); - gauge.sample(-342.23); - - Map latestMetrics = getMetricsForDimension(hostDimension); - assertEquals("Expected only 1 metric value to be set", 1, latestMetrics.size()); - assertEquals(latestMetrics.get("test.gauge"), -342.23); - } - - @Test - public void testRedeclaringSameGauge() { - Gauge gauge = metrics.declareGauge("test.gauge", hostDimension); - gauge.sample(42); - - // Same as hostDimension, but new instance. - Dimensions newDimension = new Dimensions.Builder().add("host", "abc.yahoo.com").build(); - Gauge newGauge = metrics.declareGauge("test.gauge", newDimension); - newGauge.sample(56); - - assertEquals(getMetricsForDimension(hostDimension).get("test.gauge"), 56.); - } - - @Test - public void testSameMetricNameButDifferentDimensions() { - Gauge gauge = metrics.declareGauge("test.gauge", hostDimension); - gauge.sample(42); - - // Not the same as hostDimension. - Dimensions newDimension = new Dimensions.Builder().add("host", "abcd.yahoo.com").build(); - Gauge newGauge = metrics.declareGauge("test.gauge", newDimension); - newGauge.sample(56); - - assertEquals(getMetricsForDimension(hostDimension).get("test.gauge"), 42.); - assertEquals(getMetricsForDimension(newDimension).get("test.gauge"), 56.); - } - - @Test - public void testDeletingMetric() { - metrics.declareGauge("test.gauge", hostDimension); - - Dimensions differentDimension = new Dimensions.Builder().add("host", "abcd.yahoo.com").build(); - metrics.declareGauge("test.gauge", differentDimension); - - assertEquals(2, metrics.getMetricsByType(DEFAULT).size()); - metrics.deleteMetricByDimension(APPLICATION_HOST, differentDimension, DEFAULT); - assertEquals(1, metrics.getMetricsByType(DEFAULT).size()); - assertEquals(getMetricsForDimension(hostDimension).size(), 1); - assertEquals(getMetricsForDimension(differentDimension).size(), 0); - } - - private Map getMetricsForDimension(Dimensions dimensions) { - return metrics.getOrCreateApplicationMetrics(APPLICATION_HOST, DEFAULT) - .getOrDefault(dimensions, Map.of()) - .entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().getValue())); - } -} -- cgit v1.2.3