/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.plugin.docker.machine;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.jsonrpc.commons.RequestTransmitter;
import org.eclipse.che.api.core.model.machine.MachineLogMessage;
import org.eclipse.che.api.core.model.machine.MachineStatus;
import org.eclipse.che.api.core.model.machine.ServerConf;
import org.eclipse.che.api.core.util.AbstractLineConsumer;
import org.eclipse.che.api.core.util.CompositeLineConsumer;
import org.eclipse.che.api.core.util.FileCleaner;
import org.eclipse.che.api.core.util.JsonRpcEndpointToMachineNameHolder;
import org.eclipse.che.api.core.util.JsonRpcMessageConsumer;
import org.eclipse.che.api.core.util.LineConsumer;
import org.eclipse.che.api.core.util.SystemInfo;
import org.eclipse.che.api.environment.server.MachineInstanceProvider;
import org.eclipse.che.api.environment.server.model.CheServiceImpl;
import org.eclipse.che.api.machine.server.exception.MachineException;
import org.eclipse.che.api.machine.server.exception.SourceNotFoundException;
import org.eclipse.che.api.machine.server.model.impl.MachineConfigImpl;
import org.eclipse.che.api.machine.server.model.impl.MachineImpl;
import org.eclipse.che.api.machine.server.model.impl.MachineLimitsImpl;
import org.eclipse.che.api.machine.server.model.impl.MachineLogMessageImpl;
import org.eclipse.che.api.machine.server.model.impl.MachineSourceImpl;
import org.eclipse.che.api.machine.server.spi.Instance;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.lang.Size;
import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler;
import org.eclipse.che.commons.lang.os.WindowsPathEscaper;
import org.eclipse.che.plugin.docker.client.DockerConnector;
import org.eclipse.che.plugin.docker.client.DockerConnectorProvider;
import org.eclipse.che.plugin.docker.client.ProgressLineFormatterImpl;
import org.eclipse.che.plugin.docker.client.ProgressMonitor;
import org.eclipse.che.plugin.docker.client.UserSpecificDockerRegistryCredentialsProvider;
import org.eclipse.che.plugin.docker.client.exception.ContainerNotFoundException;
import org.eclipse.che.plugin.docker.client.exception.ImageNotFoundException;
import org.eclipse.che.plugin.docker.client.exception.NetworkNotFoundException;
import org.eclipse.che.plugin.docker.client.json.ContainerConfig;
import org.eclipse.che.plugin.docker.client.json.ContainerInfo;
import org.eclipse.che.plugin.docker.client.json.Filters;
import org.eclipse.che.plugin.docker.client.json.HostConfig;
import org.eclipse.che.plugin.docker.client.json.ImageConfig;
import org.eclipse.che.plugin.docker.client.json.PortBinding;
import org.eclipse.che.plugin.docker.client.json.Volume;
import org.eclipse.che.plugin.docker.client.json.container.NetworkingConfig;
import org.eclipse.che.plugin.docker.client.json.network.ConnectContainer;
import org.eclipse.che.plugin.docker.client.json.network.EndpointConfig;
import org.eclipse.che.plugin.docker.client.json.network.NewNetwork;
import org.eclipse.che.plugin.docker.client.params.BuildImageParams;
import org.eclipse.che.plugin.docker.client.params.CreateContainerParams;
import org.eclipse.che.plugin.docker.client.params.GetContainerLogsParams;
import org.eclipse.che.plugin.docker.client.params.ListImagesParams;
import org.eclipse.che.plugin.docker.client.params.PullParams;
import org.eclipse.che.plugin.docker.client.params.RemoveContainerParams;
import org.eclipse.che.plugin.docker.client.params.RemoveImageParams;
import org.eclipse.che.plugin.docker.client.params.network.RemoveNetworkParams;
import org.eclipse.che.plugin.docker.client.params.StartContainerParams;
import org.eclipse.che.plugin.docker.client.params.TagParams;
import org.eclipse.che.plugin.docker.client.params.network.ConnectContainerToNetworkParams;
import org.eclipse.che.plugin.docker.client.params.network.CreateNetworkParams;
import org.eclipse.che.plugin.docker.machine.node.DockerNode;
import org.slf4j.Logger;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Function;
import java.util.regex.Pattern;
import static java.lang.String.format;
import static java.lang.Thread.sleep;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
import static org.eclipse.che.plugin.docker.machine.DockerInstance.LATEST_TAG;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Creates/destroys docker networks and creates docker compose based {@link Instance}.
*
* @author Alexander Garagatyi
*/
public class MachineProviderImpl implements MachineInstanceProvider {
private static final Logger LOG = getLogger(MachineProviderImpl.class);
/**
* Prefix of image repository, used to identify that the image is a machine saved to snapshot.
*/
public static final String MACHINE_SNAPSHOT_PREFIX = "machine_snapshot_";
public static final Pattern SNAPSHOT_LOCATION_PATTERN = Pattern.compile("(.+/)?" + MACHINE_SNAPSHOT_PREFIX + ".+");
static final String CONTAINER_EXITED_ERROR = "We detected that a machine exited unexpectedly. " +
"This may be caused by a container in interactive mode " +
"or a container that requires additional arguments to start. " +
"Please check the container recipe.";
// CMDs and entrypoints that lead to exiting of container right after start
private Set<List<String>> badCMDs = ImmutableSet.of(singletonList("/bin/bash"),
singletonList("/bin/sh"),
singletonList("bash"),
singletonList("sh"),
Arrays.asList("/bin/sh", "-c", "/bin/sh"),
Arrays.asList("/bin/sh", "-c", "/bin/bash"),
Arrays.asList("/bin/sh", "-c", "bash"),
Arrays.asList("/bin/sh", "-c", "sh"));
private Set<List<String>> badEntrypoints =
ImmutableSet.<List<String>>builder().addAll(badCMDs)
.add(Arrays.asList("/bin/sh", "-c"))
.add(Arrays.asList("/bin/bash", "-c"))
.add(Arrays.asList("sh", "-c"))
.add(Arrays.asList("bash", "-c"))
.build();
private final DockerConnector docker;
private final UserSpecificDockerRegistryCredentialsProvider dockerCredentials;
private final ExecutorService executor;
private final DockerInstanceStopDetector dockerInstanceStopDetector;
private final RequestTransmitter transmitter;
private final JsonRpcEndpointToMachineNameHolder jsonRpcEndpointToMachineNameHolder;
private final boolean doForcePullImage;
private final boolean privilegedMode;
private final int pidsLimit;
private final DockerMachineFactory dockerMachineFactory;
private final List<String> devMachinePortsToExpose;
private final List<String> commonMachinePortsToExpose;
private final List<String> devMachineSystemVolumes;
private final List<String> commonMachineSystemVolumes;
private final Map<String, String> devMachineEnvVariables;
private final Map<String, String> commonMachineEnvVariables;
private final String[] allMachinesExtraHosts;
private final boolean snapshotUseRegistry;
private final double memorySwapMultiplier;
private final String networkDriver;
private final Set<String> additionalNetworks;
private final String parentCgroup;
private final String cpusetCpus;
private final long cpuPeriod;
private final long cpuQuota;
private final WindowsPathEscaper windowsPathEscaper;
private final String[] dnsResolvers;
private final Map<String, String> buildArgs;
@Inject
public MachineProviderImpl(DockerConnectorProvider dockerProvider,
UserSpecificDockerRegistryCredentialsProvider dockerCredentials,
DockerMachineFactory dockerMachineFactory,
DockerInstanceStopDetector dockerInstanceStopDetector,
RequestTransmitter transmitter,
JsonRpcEndpointToMachineNameHolder jsonRpcEndpointToMachineNameHolder,
@Named("machine.docker.dev_machine.machine_servers") Set<ServerConf> devMachineServers,
@Named("machine.docker.machine_servers") Set<ServerConf> allMachinesServers,
@Named("machine.docker.dev_machine.machine_volumes") Set<String> devMachineSystemVolumes,
@Named("machine.docker.machine_volumes") Set<String> allMachinesSystemVolumes,
@Named("che.docker.always_pull_image") boolean doForcePullImage,
@Named("che.docker.privileged") boolean privilegedMode,
@Named("che.docker.pids_limit") int pidsLimit,
@Named("machine.docker.dev_machine.machine_env") Set<String> devMachineEnvVariables,
@Named("machine.docker.machine_env") Set<String> allMachinesEnvVariables,
@Named("che.docker.registry_for_snapshots") boolean snapshotUseRegistry,
@Named("che.docker.swap") double memorySwapMultiplier,
@Named("machine.docker.networks") Set<Set<String>> additionalNetworks,
@Nullable @Named("che.docker.network_driver") String networkDriver,
@Nullable @Named("che.docker.parent_cgroup") String parentCgroup,
@Nullable @Named("che.docker.cpuset_cpus") String cpusetCpus,
@Named("che.docker.cpu_period") long cpuPeriod,
@Named("che.docker.cpu_quota") long cpuQuota,
WindowsPathEscaper windowsPathEscaper,
@Named("che.docker.extra_hosts") Set<Set<String>> additionalHosts,
@Nullable @Named("che.docker.dns_resolvers") String[] dnsResolvers,
@Named("che.docker.build_args") Map<String, String> buildArgs)
throws IOException {
this.docker = dockerProvider.get();
this.dockerCredentials = dockerCredentials;
this.dockerMachineFactory = dockerMachineFactory;
this.dockerInstanceStopDetector = dockerInstanceStopDetector;
this.transmitter = transmitter;
this.doForcePullImage = doForcePullImage;
this.privilegedMode = privilegedMode;
this.snapshotUseRegistry = snapshotUseRegistry;
// use-cases:
// -1 enable unlimited swap
// 0 disable swap
// 0.5 enable swap with size equal to half of current memory size
// 1 enable swap with size equal to current memory size
//
// according to docker docs field memorySwap should be equal to memory+swap
// we calculate this field as memorySwap=memory * (1 + multiplier) so we just add 1 to multiplier
this.memorySwapMultiplier = memorySwapMultiplier == -1 ? -1 : memorySwapMultiplier + 1;
this.jsonRpcEndpointToMachineNameHolder = jsonRpcEndpointToMachineNameHolder;
this.networkDriver = networkDriver;
this.parentCgroup = parentCgroup;
this.cpusetCpus = cpusetCpus;
this.cpuPeriod = cpuPeriod;
this.cpuQuota = cpuQuota;
this.windowsPathEscaper = windowsPathEscaper;
this.pidsLimit = pidsLimit;
this.dnsResolvers = dnsResolvers;
this.buildArgs = buildArgs;
allMachinesSystemVolumes = removeEmptyAndNullValues(allMachinesSystemVolumes);
devMachineSystemVolumes = removeEmptyAndNullValues(devMachineSystemVolumes);
allMachinesSystemVolumes = allMachinesSystemVolumes.stream()
.map(line -> line.split(";"))
.flatMap(Arrays::stream)
.distinct()
.collect(toSet());
devMachineSystemVolumes = devMachineSystemVolumes.stream()
.map(line -> line.split(";"))
.flatMap(Arrays::stream)
.distinct()
.collect(toSet());
if (SystemInfo.isWindows()) {
allMachinesSystemVolumes = escapePaths(allMachinesSystemVolumes);
devMachineSystemVolumes = escapePaths(devMachineSystemVolumes);
}
this.commonMachineSystemVolumes = new ArrayList<>(allMachinesSystemVolumes);
List<String> devMachineVolumes = new ArrayList<>(allMachinesSystemVolumes.size()
+ devMachineSystemVolumes.size());
devMachineVolumes.addAll(allMachinesSystemVolumes);
devMachineVolumes.addAll(devMachineSystemVolumes);
this.devMachineSystemVolumes = devMachineVolumes;
this.devMachinePortsToExpose = new ArrayList<>(allMachinesServers.size() + devMachineServers.size());
this.commonMachinePortsToExpose = new ArrayList<>(allMachinesServers.size());
for (ServerConf serverConf : devMachineServers) {
devMachinePortsToExpose.add(serverConf.getPort());
}
for (ServerConf serverConf : allMachinesServers) {
commonMachinePortsToExpose.add(serverConf.getPort());
devMachinePortsToExpose.add(serverConf.getPort());
}
allMachinesEnvVariables = removeEmptyAndNullValues(allMachinesEnvVariables);
devMachineEnvVariables = removeEmptyAndNullValues(devMachineEnvVariables);
this.commonMachineEnvVariables = new HashMap<>();
this.devMachineEnvVariables = new HashMap<>();
allMachinesEnvVariables.forEach(envVar -> {
String[] split = envVar.split("=", 2);
this.commonMachineEnvVariables.put(split[0], split[1]);
this.devMachineEnvVariables.put(split[0], split[1]);
});
devMachineEnvVariables.forEach(envVar -> {
String[] split = envVar.split("=", 2);
this.devMachineEnvVariables.put(split[0], split[1]);
});
this.allMachinesExtraHosts = additionalHosts.stream()
.flatMap(Set::stream)
.toArray(String[]::new);
this.additionalNetworks = additionalNetworks.stream()
.flatMap(Set::stream)
.collect(toSet());
// TODO single point of failure in case of highly loaded system
executor = Executors.newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat("MachineLogsStreamer-%d")
.setUncaughtExceptionHandler(
LoggingUncaughtExceptionHandler.getInstance())
.setDaemon(true)
.build());
}
@Override
public Instance startService(String namespace,
String workspaceId,
String envName,
String machineName,
boolean isDev,
String networkName,
CheServiceImpl service,
LineConsumer machineLogger) throws ServerException {
// copy to not affect/be affected by changes in origin
service = new CheServiceImpl(service);
JsonRpcMessageConsumer<MachineLogMessage> messageConsumer =
new JsonRpcMessageConsumer<>("event:environment-output:message",
transmitter,
() -> jsonRpcEndpointToMachineNameHolder
.getEndpointIdsByWorkspaceIdPlusMachineName(workspaceId + "::" + machineName));
LineConsumer logger = new CompositeLineConsumer(machineLogger, new AbstractLineConsumer() {
@Override
public void writeLine(String line) throws IOException {
messageConsumer.consume(new MachineLogMessageImpl(machineName, line));
}
});
ProgressLineFormatterImpl progressLineFormatter = new ProgressLineFormatterImpl();
ProgressMonitor progressMonitor = currentProgressStatus -> {
try {
logger.writeLine(progressLineFormatter.format(currentProgressStatus));
} catch (IOException e) {
LOG.error(e.getLocalizedMessage(), e);
}
};
String container = null;
try {
String image = prepareImage(machineName,
service,
progressMonitor);
container = createContainer(workspaceId,
machineName,
isDev,
image,
networkName,
service);
connectContainerToAdditionalNetworks(container,
service);
docker.startContainer(StartContainerParams.create(container));
checkContainerIsRunning(container);
readContainerLogsInSeparateThread(container,
workspaceId,
service.getId(),
logger);
DockerNode node = dockerMachineFactory.createNode(workspaceId, container);
dockerInstanceStopDetector.startDetection(container,
service.getId(),
workspaceId);
final String userId = EnvironmentContext.getCurrent().getSubject().getUserId();
MachineImpl machine = new MachineImpl(MachineConfigImpl.builder()
.setDev(isDev)
.setName(machineName)
.setType("docker")
// casting considered as safe because more than int of megabytes is a lot!
.setLimits(new MachineLimitsImpl((int)Size
.parseSizeToMegabytes(
service.getMemLimit() + "b")))
.setSource(new MachineSourceImpl(service.getBuild() != null ?
"context" :
"image")
.setLocation(service.getBuild() != null ?
service.getBuild().getContext() :
service.getImage()))
.build(),
service.getId(),
workspaceId,
envName,
userId,
MachineStatus.RUNNING,
null);
return dockerMachineFactory.createInstance(machine,
container,
image,
node,
logger);
} catch (SourceNotFoundException e) {
throw e;
} catch (RuntimeException | ServerException | NotFoundException | IOException e) {
cleanUpContainer(container);
throw new ServerException(e.getLocalizedMessage(), e);
}
}
@Override
public void createNetwork(String networkName) throws ServerException {
try {
docker.createNetwork(CreateNetworkParams.create(new NewNetwork().withName(networkName)
.withDriver(networkDriver)
.withCheckDuplicate(true)));
} catch (IOException e) {
throw new ServerException(e.getLocalizedMessage(), e);
}
}
@Override
public void destroyNetwork(String networkName) throws ServerException {
try {
docker.removeNetwork(RemoveNetworkParams.create(networkName));
} catch (NetworkNotFoundException ignore) {
} catch (IOException e) {
throw new ServerException(e.getLocalizedMessage(), e);
}
}
private String prepareImage(String machineName,
CheServiceImpl service,
ProgressMonitor progressMonitor)
throws ServerException,
NotFoundException {
String imageName = "eclipse-che/" + service.getContainerName();
if ((service.getBuild() == null || (service.getBuild().getContext() == null &&
service.getBuild().getDockerfileContent() == null)) &&
service.getImage() == null) {
throw new ServerException(format("Che service '%s' doesn't have neither build nor image fields",
machineName));
}
if (service.getBuild() != null && (service.getBuild().getContext() != null ||
service.getBuild().getDockerfileContent() != null)) {
buildImage(service, imageName, doForcePullImage, progressMonitor);
} else {
pullImage(service, imageName, progressMonitor);
}
return imageName;
}
protected void buildImage(CheServiceImpl service,
String machineImageName,
boolean doForcePullOnBuild,
ProgressMonitor progressMonitor)
throws MachineException {
File workDir = null;
try {
BuildImageParams buildImageParams;
if (service.getBuild() != null &&
service.getBuild().getDockerfileContent() != null) {
workDir = Files.createTempDirectory(null).toFile();
final File dockerfileFile = new File(workDir, "Dockerfile");
try (FileWriter output = new FileWriter(dockerfileFile)) {
output.append(service.getBuild().getDockerfileContent());
}
buildImageParams = BuildImageParams.create(dockerfileFile);
} else {
buildImageParams = BuildImageParams.create(service.getBuild().getContext())
.withDockerfile(service.getBuild().getDockerfilePath());
}
Map<String, String> buildArgs;
if (service.getBuild().getArgs() == null || service.getBuild().getArgs().isEmpty()) {
buildArgs = this.buildArgs;
} else {
buildArgs = new HashMap<>(this.buildArgs);
buildArgs.putAll(service.getBuild().getArgs());
}
buildImageParams.withForceRemoveIntermediateContainers(true)
.withRepository(machineImageName)
.withAuthConfigs(dockerCredentials.getCredentials())
.withDoForcePull(doForcePullOnBuild)
.withMemoryLimit(service.getMemLimit())
.withMemorySwapLimit(-1)
.withCpusetCpus(cpusetCpus)
.withCpuPeriod(cpuPeriod)
.withCpuQuota(cpuQuota)
.withBuildArgs(buildArgs);
docker.buildImage(buildImageParams, progressMonitor);
} catch (IOException e) {
throw new MachineException(e.getLocalizedMessage(), e);
} finally {
if (workDir != null) {
FileCleaner.addFile(workDir);
}
}
}
/**
* Pulls docker image for container creation.
*
* @param service
* service that provides description of image that should be pulled
* @param machineImageName
* name of the image that should be assigned on pull
* @param progressMonitor
* consumer of output
* @throws SourceNotFoundException
* if image for pulling not found
* @throws MachineException
* if any other error occurs
*/
protected void pullImage(CheServiceImpl service,
String machineImageName,
ProgressMonitor progressMonitor) throws MachineException {
DockerMachineSource dockerMachineSource = new DockerMachineSource(
new MachineSourceImpl("image").setLocation(service.getImage()));
if (dockerMachineSource.getRepository() == null) {
throw new MachineException(
format("Machine creation failed. Machine source is invalid. No repository is defined. Found '%s'.",
dockerMachineSource));
}
try {
boolean isSnapshot = SNAPSHOT_LOCATION_PATTERN.matcher(dockerMachineSource.getLocation()).matches();
boolean isImageExistLocally = isDockerImageExistLocally(dockerMachineSource.getRepository());
if ((!isSnapshot && (doForcePullImage || !isImageExistLocally)) || (isSnapshot && snapshotUseRegistry)) {
PullParams pullParams = PullParams.create(dockerMachineSource.getRepository())
.withTag(MoreObjects.firstNonNull(dockerMachineSource.getTag(),
LATEST_TAG))
.withRegistry(dockerMachineSource.getRegistry())
.withAuthConfigs(dockerCredentials.getCredentials());
docker.pull(pullParams, progressMonitor);
}
String fullNameOfPulledImage = dockerMachineSource.getLocation(false);
try {
// tag image with generated name to allow sysadmin recognize it
docker.tag(TagParams.create(fullNameOfPulledImage, machineImageName));
} catch (ImageNotFoundException nfEx) {
throw new SourceNotFoundException(nfEx.getLocalizedMessage(), nfEx);
}
// remove unneeded tag if restoring snapshot from registry
if (isSnapshot && snapshotUseRegistry) {
docker.removeImage(RemoveImageParams.create(fullNameOfPulledImage).withForce(false));
}
} catch (IOException e) {
throw new MachineException("Can't create machine from image. Cause: " + e.getLocalizedMessage(), e);
}
}
@VisibleForTesting
boolean isDockerImageExistLocally(String imageName) {
try {
return !docker.listImages(ListImagesParams.create()
.withFilters(new Filters().withFilter("reference", imageName)))
.isEmpty();
} catch (IOException e) {
LOG.warn("Failed to check image {} availability. Cause: {}", imageName, e.getMessage(), e);
return false; // consider that image doesn't exist locally
}
}
private String createContainer(String workspaceId,
String machineName,
boolean isDev,
String image,
String networkName,
CheServiceImpl service) throws IOException {
long machineMemorySwap = memorySwapMultiplier == -1 ?
-1 :
(long)(service.getMemLimit() * memorySwapMultiplier);
addSystemWideContainerSettings(workspaceId,
machineName,
isDev,
service);
EndpointConfig endpointConfig = new EndpointConfig().withAliases(machineName)
.withLinks(toArrayIfNotNull(service.getLinks()));
NetworkingConfig networkingConfig = new NetworkingConfig().withEndpointsConfig(singletonMap(networkName,
endpointConfig));
HostConfig hostConfig = new HostConfig();
hostConfig.withMemorySwap(machineMemorySwap)
.withMemory(service.getMemLimit())
.withNetworkMode(networkName)
.withLinks(toArrayIfNotNull(service.getLinks()))
.withPortBindings(service.getPorts()
.stream()
.collect(toMap(Function.identity(), value -> new PortBinding[0])))
.withVolumesFrom(toArrayIfNotNull(service.getVolumesFrom()));
ContainerConfig config = new ContainerConfig();
config.withImage(image)
.withExposedPorts(service.getExpose()
.stream()
.distinct()
.collect(toMap(Function.identity(), value -> emptyMap())))
.withHostConfig(hostConfig)
.withCmd(toArrayIfNotNull(service.getCommand()))
.withEntrypoint(toArrayIfNotNull(service.getEntrypoint()))
.withLabels(service.getLabels())
.withNetworkingConfig(networkingConfig)
.withEnv(service.getEnvironment()
.entrySet()
.stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.toArray(String[]::new));
List<String> bindMountVolumes = new ArrayList<>();
Map<String, Volume> nonBindMountVolumes = new HashMap<>();
for (String volume : service.getVolumes()) {
// If volume contains colon then it is bind volume, otherwise - non bind-mount volume.
if (volume.contains(":")) {
bindMountVolumes.add(volume);
} else {
nonBindMountVolumes.put(volume, new Volume());
}
}
hostConfig.setBinds(bindMountVolumes.toArray(new String[bindMountVolumes.size()]));
config.setVolumes(nonBindMountVolumes);
addStaticDockerConfiguration(config);
setNonExitingContainerCommandIfNeeded(config);
return docker.createContainer(CreateContainerParams.create(config)
.withContainerName(service.getContainerName()))
.getId();
}
private void addStaticDockerConfiguration(ContainerConfig config) {
config.getHostConfig()
.withPidsLimit(pidsLimit)
.withExtraHosts(allMachinesExtraHosts)
.withPrivileged(privilegedMode)
.withPublishAllPorts(true)
.withDns(dnsResolvers);
// CPU limits
config.getHostConfig()
.withCpusetCpus(cpusetCpus)
.withCpuQuota(cpuQuota)
.withCpuPeriod(cpuPeriod);
// Cgroup parent for custom limits
config.getHostConfig().setCgroupParent(parentCgroup);
}
private void addSystemWideContainerSettings(String workspaceId,
String machineName,
boolean isDev,
CheServiceImpl composeService) throws IOException {
List<String> portsToExpose;
List<String> volumes;
Map<String, String> env;
if (isDev) {
portsToExpose = devMachinePortsToExpose;
volumes = devMachineSystemVolumes;
env = new HashMap<>(devMachineEnvVariables);
env.put(DockerInstanceRuntimeInfo.USER_TOKEN, getUserToken(workspaceId));
} else {
portsToExpose = commonMachinePortsToExpose;
env = new HashMap<>(commonMachineEnvVariables);
volumes = commonMachineSystemVolumes;
}
// register workspace ID and Machine Name
env.put(DockerInstanceRuntimeInfo.CHE_WORKSPACE_ID, workspaceId);
env.put(DockerInstanceRuntimeInfo.CHE_MACHINE_NAME, machineName);
composeService.getExpose().addAll(portsToExpose);
composeService.getEnvironment().putAll(env);
composeService.getVolumes().addAll(volumes);
composeService.getNetworks().addAll(additionalNetworks);
}
// We can detect certain situation when container exited right after start.
// We can detect
// - when no command/entrypoint is set
// - when most common shell interpreters are used and require additional arguments
// - when most common shell interpreters are used and they require interactive mode which we don't support
// When we identify such situation we change CMD/entrypoint in such a way that it runs "tail -f /dev/null".
// This command does nothing and lasts until workspace is stopped.
// Images such as "ubuntu" or "openjdk" fits this situation.
protected void setNonExitingContainerCommandIfNeeded(ContainerConfig containerConfig) throws IOException {
ImageConfig imageConfig = docker.inspectImage(containerConfig.getImage()).getConfig();
List<String> cmd = imageConfig.getCmd() == null ?
null : Arrays.asList(imageConfig.getCmd());
List<String> entrypoint = imageConfig.getEntrypoint() == null ?
null : Arrays.asList(imageConfig.getEntrypoint());
if ((entrypoint == null || badEntrypoints.contains(entrypoint)) && (cmd == null || badCMDs.contains(cmd))) {
containerConfig.setCmd("tail", "-f", "/dev/null");
containerConfig.setEntrypoint((String[])null);
}
}
// Inspect container right after start to check if it is running,
// otherwise throw error that command should not exit right after container start
protected void checkContainerIsRunning(String container) throws IOException, ServerException {
ContainerInfo containerInfo = docker.inspectContainer(container);
if ("exited".equals(containerInfo.getState().getStatus())) {
throw new ServerException(CONTAINER_EXITED_ERROR);
}
}
private void connectContainerToAdditionalNetworks(String container,
CheServiceImpl service) throws IOException {
for (String network : service.getNetworks()) {
docker.connectContainerToNetwork(
ConnectContainerToNetworkParams.create(network, new ConnectContainer().withContainer(container)));
}
}
private void readContainerLogsInSeparateThread(String container,
String workspaceId,
String machineId,
LineConsumer outputConsumer) {
executor.execute(() -> {
long lastProcessedLogDate = 0;
boolean isContainerRunning = true;
int errorsCounter = 0;
long lastErrorTime = 0;
while (isContainerRunning) {
try {
docker.getContainerLogs(GetContainerLogsParams.create(container)
.withFollow(true)
.withSince(lastProcessedLogDate),
new LogMessagePrinter(outputConsumer));
isContainerRunning = false;
} catch (SocketTimeoutException ste) {
lastProcessedLogDate = System.currentTimeMillis() / 1000L;
// reconnect to container
} catch (ContainerNotFoundException e) {
isContainerRunning = false;
} catch (IOException e) {
long errorTime = System.currentTimeMillis();
lastProcessedLogDate = errorTime / 1000L;
LOG.warn("Failed to get logs from machine {} of workspace {} backed by container {}, because: {}.",
machineId,
workspaceId,
container,
e.getMessage(),
e);
if (errorTime - lastErrorTime < 20_000L) { // if new error occurs less than 20 seconds after previous
if (++errorsCounter == 5) {
LOG.error("Too many errors while streaming logs from machine {} of workspace {} backed by container {}. " +
"Logs streaming is closed. Last error: {}.",
machineId,
workspaceId,
container,
e.getMessage(),
e);
break;
}
} else {
errorsCounter = 1;
}
lastErrorTime = errorTime;
try {
sleep(1_000);
} catch (InterruptedException ie) {
return;
}
}
}
});
}
private void cleanUpContainer(String containerId) {
try {
if (containerId != null) {
docker.removeContainer(RemoveContainerParams.create(containerId)
.withRemoveVolumes(true)
.withForce(true));
}
} catch (Exception ex) {
LOG.error("Failed to remove docker container {}", containerId, ex);
}
}
// workspaceId parameter is required, because in case of separate storage for tokens
// you need to know exactly which workspace and which user to apply the token.
protected String getUserToken(String wsId) {
return EnvironmentContext.getCurrent().getSubject().getToken();
}
/**
* Returns set that contains all non empty and non nullable values from specified set
*/
protected Set<String> removeEmptyAndNullValues(Set<String> paths) {
return paths.stream()
.filter(path -> !Strings.isNullOrEmpty(path))
.collect(toSet());
}
/**
* Escape paths for Windows system with boot@docker according to rules given here :
* https://github.com/boot2docker/boot2docker/blob/master/README.md#virtualbox-guest-additions
*
* @param paths
* set of paths to escape
* @return set of escaped path
*/
private Set<String> escapePaths(Set<String> paths) {
return paths.stream()
.map(windowsPathEscaper::escapePath)
.collect(toSet());
}
/** Converts list to array if it is not null, otherwise returns null */
private String[] toArrayIfNotNull(List<String> list) {
if (list == null) {
return null;
}
return list.toArray(new String[list.size()]);
}
}