Enabe parallel pull of images in docker infra

6.19.x
Max Shaposhnik 2018-02-19 10:53:24 +02:00 committed by GitHub
parent 985b895628
commit df2f7ad4e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 547 additions and 186 deletions

View File

@ -263,6 +263,10 @@ che.infra.docker.bootstrapper.installer_timeout_sec=180
# Once servers for one installer available - checks stopped.
che.infra.docker.bootstrapper.server_check_period_sec=3
# Number of threads to build or pull docker images
# in parallel on workspace startups.
che.infra.docker.max_pull_threads=10
# Single port mode
che.single.port=false

View File

@ -70,6 +70,7 @@ public class DockerInfraModule extends AbstractModule {
install(new FactoryModuleBuilder().build(DockerRuntimeFactory.class));
install(new FactoryModuleBuilder().build(DockerBootstrapperFactory.class));
install(new FactoryModuleBuilder().build(DockerRuntimeContextFactory.class));
install(new FactoryModuleBuilder().build(ParallelDockerImagesBuilderFactory.class));
bind(
org.eclipse.che.workspace.infrastructure.docker.monit.DockerAbandonedResourcesCleaner
.class);

View File

@ -76,6 +76,7 @@ public class DockerInternalRuntime extends InternalRuntime<DockerRuntimeContext>
private final MachineLoggersFactory loggers;
private final ProbeScheduler probeScheduler;
private final WorkspaceProbesFactory probesFactory;
private final ParallelDockerImagesBuilderFactory imagesBuilderFactory;
/**
* Creates non running runtime. Normally created by {@link
@ -93,7 +94,8 @@ public class DockerInternalRuntime extends InternalRuntime<DockerRuntimeContext>
ServersCheckerFactory serverCheckerFactory,
MachineLoggersFactory loggers,
ProbeScheduler probeScheduler,
WorkspaceProbesFactory probesFactory) {
WorkspaceProbesFactory probesFactory,
ParallelDockerImagesBuilderFactory imagesBuilderFactory) {
this(
context,
urlRewriter,
@ -106,7 +108,8 @@ public class DockerInternalRuntime extends InternalRuntime<DockerRuntimeContext>
serverCheckerFactory,
loggers,
probeScheduler,
probesFactory);
probesFactory,
imagesBuilderFactory);
}
/**
@ -128,7 +131,8 @@ public class DockerInternalRuntime extends InternalRuntime<DockerRuntimeContext>
DockerMachineCreator machineCreator,
DockerMachineStopDetector stopDetector,
ProbeScheduler probeScheduler,
WorkspaceProbesFactory probesFactory)
WorkspaceProbesFactory probesFactory,
ParallelDockerImagesBuilderFactory imagesBuilderFactory)
throws InfrastructureException {
this(
context,
@ -142,7 +146,8 @@ public class DockerInternalRuntime extends InternalRuntime<DockerRuntimeContext>
serverCheckerFactory,
loggers,
probeScheduler,
probesFactory);
probesFactory,
imagesBuilderFactory);
for (ContainerListEntry container : containers) {
DockerMachine machine = machineCreator.create(container);
@ -166,7 +171,8 @@ public class DockerInternalRuntime extends InternalRuntime<DockerRuntimeContext>
ServersCheckerFactory serverCheckerFactory,
MachineLoggersFactory loggers,
ProbeScheduler probeScheduler,
WorkspaceProbesFactory probesFactory) {
WorkspaceProbesFactory probesFactory,
ParallelDockerImagesBuilderFactory imagesBuilderFactory) {
super(context, urlRewriter, warnings, running);
this.networks = networks;
this.containerStarter = machineStarter;
@ -179,6 +185,7 @@ public class DockerInternalRuntime extends InternalRuntime<DockerRuntimeContext>
this.runtimeMachines = new RuntimeMachines();
this.loggers = loggers;
this.probeScheduler = probeScheduler;
this.imagesBuilderFactory = imagesBuilderFactory;
}
@Override
@ -186,6 +193,10 @@ public class DockerInternalRuntime extends InternalRuntime<DockerRuntimeContext>
startSynchronizer.setStartThread();
try {
networks.createNetwork(getContext().getEnvironment().getNetwork());
Map<String, String> images =
imagesBuilderFactory
.create(getContext().getIdentity())
.prepareImages(getContext().getEnvironment().getContainers());
for (Map.Entry<String, DockerContainerConfig> containerEntry :
getContext().getEnvironment().getContainers().entrySet()) {
@ -196,7 +207,8 @@ public class DockerInternalRuntime extends InternalRuntime<DockerRuntimeContext>
sendStartingEvent(machineName);
try {
DockerMachine machine = startMachine(machineName, containerEntry.getValue());
DockerMachine machine =
startMachine(machineName, images.get(machineName), containerEntry.getValue());
sendRunningEvent(machineName);
bootstrapInstallers(machineName, machine);
@ -289,7 +301,8 @@ public class DockerInternalRuntime extends InternalRuntime<DockerRuntimeContext>
}
}
private DockerMachine startMachine(String name, DockerContainerConfig containerConfig)
private DockerMachine startMachine(
String name, String image, DockerContainerConfig containerConfig)
throws InfrastructureException, InterruptedException {
RuntimeIdentity identity = getContext().getIdentity();
@ -297,6 +310,7 @@ public class DockerInternalRuntime extends InternalRuntime<DockerRuntimeContext>
containerStarter.startContainer(
getContext().getEnvironment().getNetwork(),
name,
image,
containerConfig,
identity,
new AbnormalMachineStopHandlerImpl());

View File

@ -16,19 +16,14 @@ 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 org.eclipse.che.workspace.infrastructure.docker.DockerMachine.LATEST_TAG;
import static org.slf4j.LoggerFactory.getLogger;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
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;
@ -39,23 +34,16 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Function;
import javax.inject.Inject;
import javax.inject.Named;
import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity;
import org.eclipse.che.api.core.util.FileCleaner;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.InternalInfrastructureException;
import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler;
import org.eclipse.che.infrastructure.docker.client.DockerConnector;
import org.eclipse.che.infrastructure.docker.client.DockerFileException;
import org.eclipse.che.infrastructure.docker.client.LogMessage;
import org.eclipse.che.infrastructure.docker.client.MessageProcessor;
import org.eclipse.che.infrastructure.docker.client.ProgressMonitor;
import org.eclipse.che.infrastructure.docker.client.UserSpecificDockerRegistryCredentialsProvider;
import org.eclipse.che.infrastructure.docker.client.exception.ContainerNotFoundException;
import org.eclipse.che.infrastructure.docker.client.exception.ImageNotFoundException;
import org.eclipse.che.infrastructure.docker.client.json.ContainerConfig;
import org.eclipse.che.infrastructure.docker.client.json.ContainerInfo;
import org.eclipse.che.infrastructure.docker.client.json.Filters;
import org.eclipse.che.infrastructure.docker.client.json.HostConfig;
import org.eclipse.che.infrastructure.docker.client.json.ImageConfig;
import org.eclipse.che.infrastructure.docker.client.json.PortBinding;
@ -63,17 +51,11 @@ import org.eclipse.che.infrastructure.docker.client.json.Volume;
import org.eclipse.che.infrastructure.docker.client.json.container.NetworkingConfig;
import org.eclipse.che.infrastructure.docker.client.json.network.ConnectContainer;
import org.eclipse.che.infrastructure.docker.client.json.network.EndpointConfig;
import org.eclipse.che.infrastructure.docker.client.params.BuildImageParams;
import org.eclipse.che.infrastructure.docker.client.params.CreateContainerParams;
import org.eclipse.che.infrastructure.docker.client.params.GetContainerLogsParams;
import org.eclipse.che.infrastructure.docker.client.params.ListImagesParams;
import org.eclipse.che.infrastructure.docker.client.params.PullParams;
import org.eclipse.che.infrastructure.docker.client.params.RemoveContainerParams;
import org.eclipse.che.infrastructure.docker.client.params.StartContainerParams;
import org.eclipse.che.infrastructure.docker.client.params.TagParams;
import org.eclipse.che.infrastructure.docker.client.params.network.ConnectContainerToNetworkParams;
import org.eclipse.che.infrastructure.docker.client.parser.DockerImageIdentifier;
import org.eclipse.che.infrastructure.docker.client.parser.DockerImageIdentifierParser;
import org.eclipse.che.workspace.infrastructure.docker.exception.SourceNotFoundException;
import org.eclipse.che.workspace.infrastructure.docker.logs.MachineLoggersFactory;
import org.eclipse.che.workspace.infrastructure.docker.model.DockerContainerConfig;
@ -152,28 +134,24 @@ public class DockerMachineStarter {
.build();
private final DockerConnector docker;
private final UserSpecificDockerRegistryCredentialsProvider dockerCredentials;
private final ExecutorService executor;
private final DockerMachineStopDetector dockerInstanceStopDetector;
private final boolean doForcePullImage;
private final MachineLoggersFactory machineLoggerFactory;
private final DockerMachineCreator machineCreator;
@Inject
public DockerMachineStarter(
DockerConnector docker,
UserSpecificDockerRegistryCredentialsProvider dockerCredentials,
DockerMachineStopDetector dockerMachineStopDetector,
@Named("che.docker.always_pull_image") boolean doForcePullImage,
MachineLoggersFactory machineLogger,
DockerMachineCreator machineCreator) {
this.machineCreator = machineCreator;
// TODO spi should we move all configuration stuff into infrastructure provisioner and left
// logic of container start here only
this.docker = docker;
this.dockerCredentials = dockerCredentials;
this.dockerInstanceStopDetector = dockerMachineStopDetector;
this.doForcePullImage = doForcePullImage;
this.machineLoggerFactory = machineLogger;
// single point of failure in case of highly loaded system
executor =
@ -201,6 +179,7 @@ public class DockerMachineStarter {
public DockerMachine startContainer(
String networkName,
String machineName,
String image,
DockerContainerConfig containerConfig,
RuntimeIdentity identity,
AbnormalMachineStopHandler abnormalMachineStopHandler)
@ -209,12 +188,8 @@ public class DockerMachineStarter {
// copy to not affect/be affected by changes in origin
containerConfig = new DockerContainerConfig(containerConfig);
final ProgressMonitor progressMonitor =
machineLoggerFactory.newProgressMonitor(machineName, identity);
String container = null;
try {
String image = prepareImage(machineName, containerConfig, progressMonitor);
container = createContainer(machineName, image, networkName, containerConfig);
connectContainerToAdditionalNetworks(container, containerConfig);
@ -242,150 +217,6 @@ public class DockerMachineStarter {
}
}
private String prepareImage(
String machineName, DockerContainerConfig container, ProgressMonitor progressMonitor)
throws SourceNotFoundException, InternalInfrastructureException {
String imageName = "eclipse-che/" + container.getContainerName();
if ((container.getBuild() == null
|| (container.getBuild().getContext() == null
&& container.getBuild().getDockerfileContent() == null))
&& container.getImage() == null) {
throw new InternalInfrastructureException(
format("Che container '%s' doesn't have neither build nor image fields", machineName));
}
if (container.getBuild() != null
&& (container.getBuild().getContext() != null
|| container.getBuild().getDockerfileContent() != null)) {
buildImage(container, imageName, doForcePullImage, progressMonitor);
} else {
pullImage(container, imageName, progressMonitor);
}
return imageName;
}
/**
* Builds Docker image for container creation.
*
* @param containerConfig configuration of container
* @param machineImageName name of image that should be applied to built image
* @param doForcePullOnBuild whether re-pulling of base image should be performed when it exists
* locally
* @param progressMonitor consumer of build logs
* @throws InternalInfrastructureException when any error occurs
*/
protected void buildImage(
DockerContainerConfig containerConfig,
String machineImageName,
boolean doForcePullOnBuild,
ProgressMonitor progressMonitor)
throws InternalInfrastructureException {
File workDir = null;
try {
BuildImageParams buildImageParams;
if (containerConfig.getBuild() != null
&& containerConfig.getBuild().getDockerfileContent() != null) {
workDir = Files.createTempDirectory(null).toFile();
final File dockerfileFile = new File(workDir, "Dockerfile");
try (FileWriter output = new FileWriter(dockerfileFile)) {
output.append(containerConfig.getBuild().getDockerfileContent());
}
buildImageParams = BuildImageParams.create(dockerfileFile);
} else {
buildImageParams =
BuildImageParams.create(containerConfig.getBuild().getContext())
.withDockerfile(containerConfig.getBuild().getDockerfilePath());
}
buildImageParams
.withForceRemoveIntermediateContainers(true)
.withRepository(machineImageName)
.withAuthConfigs(dockerCredentials.getCredentials())
.withDoForcePull(doForcePullOnBuild)
.withMemoryLimit(containerConfig.getMemLimit())
.withMemorySwapLimit(-1)
.withBuildArgs(containerConfig.getBuild().getArgs());
docker.buildImage(buildImageParams, progressMonitor);
} catch (IOException e) {
throw new InternalInfrastructureException(e.getLocalizedMessage(), e);
} finally {
if (workDir != null) {
FileCleaner.addFile(workDir);
}
}
}
/**
* Pulls docker image for container creation.
*
* @param container container 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 in registry
* @throws InternalInfrastructureException if any other error occurs
*/
protected void pullImage(
DockerContainerConfig container, String machineImageName, ProgressMonitor progressMonitor)
throws InternalInfrastructureException, SourceNotFoundException {
final DockerImageIdentifier dockerImageIdentifier;
try {
dockerImageIdentifier = DockerImageIdentifierParser.parse(container.getImage());
} catch (DockerFileException e) {
throw new InternalInfrastructureException(
"Try to build a docker machine source with an invalid location/content. It is not in the expected format",
e);
}
if (dockerImageIdentifier.getRepository() == null) {
throw new InternalInfrastructureException(
format(
"Machine creation failed. Machine source is invalid. No repository is defined. Found '%s'.",
dockerImageIdentifier.getRepository()));
}
try {
boolean isImageExistLocally =
isDockerImageExistLocally(dockerImageIdentifier.getRepository());
if (doForcePullImage || !isImageExistLocally) {
PullParams pullParams =
PullParams.create(dockerImageIdentifier.getRepository())
.withTag(MoreObjects.firstNonNull(dockerImageIdentifier.getTag(), LATEST_TAG))
.withRegistry(dockerImageIdentifier.getRegistry())
.withAuthConfigs(dockerCredentials.getCredentials());
docker.pull(pullParams, progressMonitor);
}
String fullNameOfPulledImage = container.getImage();
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);
}
} catch (IOException e) {
throw new InternalInfrastructureException(
"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 machineName, String image, String networkName, DockerContainerConfig containerConfig)
throws IOException, InternalInfrastructureException {

View File

@ -0,0 +1,306 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* 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:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.docker;
import static java.lang.String.format;
import static java.util.stream.Collectors.toList;
import static org.eclipse.che.workspace.infrastructure.docker.DockerMachine.LATEST_TAG;
import static org.slf4j.LoggerFactory.getLogger;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.inject.assistedinject.Assisted;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.function.Supplier;
import javax.inject.Inject;
import javax.inject.Named;
import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity;
import org.eclipse.che.api.core.util.FileCleaner;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.InternalInfrastructureException;
import org.eclipse.che.infrastructure.docker.client.DockerConnector;
import org.eclipse.che.infrastructure.docker.client.DockerFileException;
import org.eclipse.che.infrastructure.docker.client.ProgressMonitor;
import org.eclipse.che.infrastructure.docker.client.UserSpecificDockerRegistryCredentialsProvider;
import org.eclipse.che.infrastructure.docker.client.exception.ImageNotFoundException;
import org.eclipse.che.infrastructure.docker.client.json.Filters;
import org.eclipse.che.infrastructure.docker.client.params.BuildImageParams;
import org.eclipse.che.infrastructure.docker.client.params.ListImagesParams;
import org.eclipse.che.infrastructure.docker.client.params.PullParams;
import org.eclipse.che.infrastructure.docker.client.params.TagParams;
import org.eclipse.che.infrastructure.docker.client.parser.DockerImageIdentifier;
import org.eclipse.che.infrastructure.docker.client.parser.DockerImageIdentifierParser;
import org.eclipse.che.workspace.infrastructure.docker.exception.SourceNotFoundException;
import org.eclipse.che.workspace.infrastructure.docker.logs.MachineLoggersFactory;
import org.eclipse.che.workspace.infrastructure.docker.model.DockerContainerConfig;
import org.slf4j.Logger;
/**
* This class allows to make parallel prepare (build or download) of docker images for workspace
* being started.
*
* @author Max Shaposhnik (mshaposh@redhat.com)
*/
public class ParallelDockerImagesBuilder {
private static final Logger LOG = getLogger(ParallelDockerImagesBuilder.class);
private static final String PARALLEL_PULL_PROPERTY_NAME = "che.infra.docker.max_pull_threads";
private final RuntimeIdentity identity;
private final MachineLoggersFactory machineLoggersFactory;
private final boolean doForcePullImage;
private final UserSpecificDockerRegistryCredentialsProvider dockerCredentials;
private final DockerConnector dockerConnector;
private final ThreadPoolExecutor executor;
@Inject
public ParallelDockerImagesBuilder(
@Assisted RuntimeIdentity identity,
@Named("che.docker.always_pull_image") boolean doForcePullImage,
@Named(PARALLEL_PULL_PROPERTY_NAME) int parallelPullsNumber,
UserSpecificDockerRegistryCredentialsProvider dockerCredentials,
DockerConnector dockerConnector,
MachineLoggersFactory machineLoggersFactory) {
this.identity = identity;
this.doForcePullImage = doForcePullImage;
this.dockerCredentials = dockerCredentials;
this.dockerConnector = dockerConnector;
this.machineLoggersFactory = machineLoggersFactory;
ThreadFactory factory =
new ThreadFactoryBuilder()
.setNameFormat(getClass().getSimpleName() + "-%d")
.setDaemon(true)
.build();
this.executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(parallelPullsNumber, factory);
}
/**
* Schedule parallel preparation of docker images for the set of docker containers.
*
* @param containers map of machine name and it's container config
* @return map of machine names and theirs image names.
* @throws InterruptedException if process is interrupted
* @throws InfrastructureException if build failed
*/
public Map<String, String> prepareImages(Map<String, DockerContainerConfig> containers)
throws InterruptedException, InfrastructureException {
if (executor.getActiveCount() + containers.size() > executor.getMaximumPoolSize()) {
LOG.warn(
String.format(
"Maximum parallel images preparing threads reached. Some images will be queued.\n"
+ " Workspace machines count is %s. If problem persists, increase %s property value.",
containers.size(), PARALLEL_PULL_PROPERTY_NAME));
}
Map<String, String> machineToImageNames = new ConcurrentHashMap<>(containers.size());
CompletableFuture<Void> firstFailed = new CompletableFuture<>();
List<CompletableFuture<Void>> taskFutures =
containers
.entrySet()
.stream()
.map(
e ->
CompletableFuture.supplyAsync(
(Supplier<Void>)
() -> {
try {
machineToImageNames.put(
e.getKey(), prepareImage(e.getKey(), e.getValue()));
} catch (InternalInfrastructureException
| SourceNotFoundException ex) {
firstFailed.completeExceptionally(ex);
}
return null;
},
executor))
.collect(toList());
CompletableFuture all =
CompletableFuture.allOf(taskFutures.toArray(new CompletableFuture[taskFutures.size()]));
try {
CompletableFuture.anyOf(all, firstFailed).get();
} catch (ExecutionException e) {
try {
throw e.getCause();
} catch (InfrastructureException rethrow) {
throw rethrow;
} catch (Throwable thr) {
throw new InternalInfrastructureException("Unable to build or pull image", thr);
}
}
return machineToImageNames;
}
/**
* Prepares (builds or downloads) docker image for container config.
*
* @param container container config
* @return name of the image for the given container config
* @throws InternalInfrastructureException if config is incomplete or image build failed
* @throws SourceNotFoundException if image pull failed
*/
private String prepareImage(String machineName, DockerContainerConfig container)
throws SourceNotFoundException, InternalInfrastructureException {
ProgressMonitor progressMonitor =
machineLoggersFactory.newProgressMonitor(machineName, identity);
final String imageName = "eclipse-che/" + container.getContainerName();
if ((container.getBuild() == null
|| (container.getBuild().getContext() == null
&& container.getBuild().getDockerfileContent() == null))
&& container.getImage() == null) {
throw new InternalInfrastructureException(
format("Che container '%s' doesn't have neither build nor image fields", machineName));
}
if (container.getBuild() != null
&& (container.getBuild().getContext() != null
|| container.getBuild().getDockerfileContent() != null)) {
buildImage(container, imageName, doForcePullImage, progressMonitor);
} else {
pullImage(container, imageName, progressMonitor);
}
return imageName;
}
/**
* Builds Docker image for container creation.
*
* @param containerConfig configuration of container
* @param machineImageName name of image that should be applied to built image
* @param doForcePullOnBuild whether re-pulling of base image should be performed when it exists
* locally
* @param progressMonitor consumer of build logs
* @throws InternalInfrastructureException when any error occurs
*/
private void buildImage(
DockerContainerConfig containerConfig,
String machineImageName,
boolean doForcePullOnBuild,
ProgressMonitor progressMonitor)
throws InternalInfrastructureException {
File workDir = null;
try {
BuildImageParams buildImageParams;
if (containerConfig.getBuild() != null
&& containerConfig.getBuild().getDockerfileContent() != null) {
workDir = Files.createTempDirectory(null).toFile();
final File dockerfileFile = new File(workDir, "Dockerfile");
try (FileWriter output = new FileWriter(dockerfileFile)) {
output.append(containerConfig.getBuild().getDockerfileContent());
}
buildImageParams = BuildImageParams.create(dockerfileFile);
} else {
buildImageParams =
BuildImageParams.create(containerConfig.getBuild().getContext())
.withDockerfile(containerConfig.getBuild().getDockerfilePath());
}
buildImageParams
.withForceRemoveIntermediateContainers(true)
.withRepository(machineImageName)
.withAuthConfigs(dockerCredentials.getCredentials())
.withDoForcePull(doForcePullOnBuild)
.withMemoryLimit(containerConfig.getMemLimit())
.withMemorySwapLimit(-1)
.withBuildArgs(containerConfig.getBuild().getArgs());
dockerConnector.buildImage(buildImageParams, progressMonitor);
} catch (IOException e) {
throw new InternalInfrastructureException(e.getLocalizedMessage(), e);
} finally {
if (workDir != null) {
FileCleaner.addFile(workDir);
}
}
}
/**
* Pulls docker image for container creation.
*
* @param container container 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 in registry
* @throws InternalInfrastructureException if any other error occurs
*/
private void pullImage(
DockerContainerConfig container, String machineImageName, ProgressMonitor progressMonitor)
throws InternalInfrastructureException, SourceNotFoundException {
final DockerImageIdentifier dockerImageIdentifier;
try {
dockerImageIdentifier = DockerImageIdentifierParser.parse(container.getImage());
} catch (DockerFileException e) {
throw new InternalInfrastructureException(
"Try to build a docker machine source with an invalid location/content. It is not in the expected format",
e);
}
if (dockerImageIdentifier.getRepository() == null) {
throw new InternalInfrastructureException(
format(
"Machine creation failed. Machine source is invalid. No repository is defined. Found '%s'.",
dockerImageIdentifier.getRepository()));
}
try {
boolean isImageExistLocally =
isDockerImageExistLocally(dockerImageIdentifier.getRepository());
if (doForcePullImage || !isImageExistLocally) {
PullParams pullParams =
PullParams.create(dockerImageIdentifier.getRepository())
.withTag(MoreObjects.firstNonNull(dockerImageIdentifier.getTag(), LATEST_TAG))
.withRegistry(dockerImageIdentifier.getRegistry())
.withAuthConfigs(dockerCredentials.getCredentials());
dockerConnector.pull(pullParams, progressMonitor);
}
String fullNameOfPulledImage = container.getImage();
try {
// tag image with generated name to allow sysadmin recognize it
dockerConnector.tag(TagParams.create(fullNameOfPulledImage, machineImageName));
} catch (ImageNotFoundException nfEx) {
throw new SourceNotFoundException(nfEx.getLocalizedMessage(), nfEx);
}
} catch (IOException e) {
throw new InternalInfrastructureException(
"Can't create machine from image. Cause: " + e.getLocalizedMessage(), e);
}
}
@VisibleForTesting
boolean isDockerImageExistLocally(String imageName) {
try {
return !dockerConnector
.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
}
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* 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:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.docker;
import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
/** @author Max Shaposhnik (mshaposh@redhat.com) */
public interface ParallelDockerImagesBuilderFactory {
ParallelDockerImagesBuilder create(RuntimeIdentity identity) throws InfrastructureException;
}

View File

@ -20,6 +20,7 @@ import static org.eclipse.che.api.core.model.workspace.runtime.MachineStatus.STA
import static org.eclipse.che.api.core.model.workspace.runtime.MachineStatus.STOPPED;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
@ -101,6 +102,8 @@ public class DockerInternalRuntimeTest {
@Mock private ProbeScheduler probesScheduler;
@Mock private WorkspaceProbes workspaceProbes;
@Mock private DockerMachine dockerMachine;
@Mock private ParallelDockerImagesBuilderFactory dockerImagesBuilderFactory;
@Mock private ParallelDockerImagesBuilder dockerImagesBuilder;
@Captor private ArgumentCaptor<Consumer<ProbeResult>> probeResultConsumerCaptor;
@Captor private ArgumentCaptor<MachineStatusEvent> eventCaptor;
@ -133,6 +136,8 @@ public class DockerInternalRuntimeTest {
.thenReturn(mock(ServersChecker.class));
when(workspaceProbesFactory.getProbes(eq(IDENTITY.getWorkspaceId()), anyString(), any()))
.thenReturn(workspaceProbes);
when(dockerImagesBuilderFactory.create(any())).thenReturn(dockerImagesBuilder);
when(dockerImagesBuilder.prepareImages(anyMap())).thenReturn(emptyMap());
dockerRuntime =
new DockerInternalRuntime(
runtimeContext,
@ -145,7 +150,8 @@ public class DockerInternalRuntimeTest {
serversCheckerFactory,
mock(MachineLoggersFactory.class),
probesScheduler,
workspaceProbesFactory);
workspaceProbesFactory,
dockerImagesBuilderFactory);
}
@Test
@ -155,7 +161,13 @@ public class DockerInternalRuntimeTest {
dockerRuntime.start(emptyMap());
verify(starter, times(2))
.startContainer(nullable(String.class), nullable(String.class), any(), any(), any());
.startContainer(
nullable(String.class),
nullable(String.class),
nullable(String.class),
any(),
any(),
any());
verify(eventService, times(4)).publish(any(MachineStatusEvent.class));
verifyEventsOrder(
newEvent(DEV_MACHINE, STARTING, null),
@ -174,7 +186,13 @@ public class DockerInternalRuntimeTest {
dockerRuntime.start(emptyMap());
} catch (InfrastructureException ex) {
verify(starter, times(1))
.startContainer(nullable(String.class), nullable(String.class), any(), any(), any());
.startContainer(
nullable(String.class),
nullable(String.class),
nullable(String.class),
any(),
any(),
any());
verify(eventService, times(3)).publish(any(MachineStatusEvent.class));
verifyEventsOrder(
newEvent(DEV_MACHINE, STARTING, null),
@ -192,7 +210,13 @@ public class DockerInternalRuntimeTest {
dockerRuntime.start(emptyMap());
} catch (InfrastructureException ex) {
verify(starter, times(1))
.startContainer(nullable(String.class), nullable(String.class), any(), any(), any());
.startContainer(
nullable(String.class),
nullable(String.class),
nullable(String.class),
any(),
any(),
any());
verify(bootstrapper, times(1)).bootstrap();
verify(eventService, times(4)).publish(any(MachineStatusEvent.class));
verifyEventsOrder(
@ -215,7 +239,13 @@ public class DockerInternalRuntimeTest {
dockerRuntime.start(emptyMap());
} catch (InfrastructureException ex) {
verify(starter, never())
.startContainer(nullable(String.class), nullable(String.class), any(), any(), any());
.startContainer(
nullable(String.class),
nullable(String.class),
nullable(String.class),
any(),
any(),
any());
throw ex;
}
}
@ -229,7 +259,13 @@ public class DockerInternalRuntimeTest {
dockerRuntime.start(emptyMap());
} catch (InfrastructureException ex) {
verify(starter, times(1))
.startContainer(nullable(String.class), nullable(String.class), any(), any(), any());
.startContainer(
nullable(String.class),
nullable(String.class),
nullable(String.class),
any(),
any(),
any());
throw ex;
}
}
@ -246,6 +282,7 @@ public class DockerInternalRuntimeTest {
})
.when(starter)
.startContainer(
nullable(String.class),
nullable(String.class),
nullable(String.class),
any(DockerContainerConfig.class),
@ -256,7 +293,13 @@ public class DockerInternalRuntimeTest {
dockerRuntime.start(emptyMap());
} catch (InfrastructureException ex) {
verify(starter, times(1))
.startContainer(nullable(String.class), nullable(String.class), any(), any(), any());
.startContainer(
nullable(String.class),
nullable(String.class),
nullable(String.class),
any(),
any(),
any());
throw ex;
}
}
@ -384,6 +427,7 @@ public class DockerInternalRuntimeTest {
private void mockContainerStart() throws InfrastructureException {
when(starter.startContainer(
nullable(String.class),
nullable(String.class),
nullable(String.class),
nullable(DockerContainerConfig.class),
@ -395,6 +439,7 @@ public class DockerInternalRuntimeTest {
private void mockContainerStartFailed(InfrastructureException exception)
throws InfrastructureException {
when(starter.startContainer(
nullable(String.class),
nullable(String.class),
nullable(String.class),
nullable(DockerContainerConfig.class),

View File

@ -0,0 +1,140 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* 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:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.docker;
import static java.util.Collections.singletonMap;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertTrue;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.infrastructure.docker.client.DockerConnector;
import org.eclipse.che.infrastructure.docker.client.UserSpecificDockerRegistryCredentialsProvider;
import org.eclipse.che.infrastructure.docker.client.params.BuildImageParams;
import org.eclipse.che.workspace.infrastructure.docker.logs.MachineLoggersFactory;
import org.eclipse.che.workspace.infrastructure.docker.model.DockerBuildContext;
import org.eclipse.che.workspace.infrastructure.docker.model.DockerContainerConfig;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
/** @author Max Shaposhnik (mshaposh@redhat.com) */
@Listeners(MockitoTestNGListener.class)
public class ParallelDockerImagesBuilderTest {
@Mock private RuntimeIdentity identity;
@Mock private UserSpecificDockerRegistryCredentialsProvider dockerCredentials;
@Mock private DockerConnector dockerConnector;
@Mock private MachineLoggersFactory machineLoggersFactory;
private ParallelDockerImagesBuilder dockerImagesBuilder;
@BeforeMethod
public void setUp() throws Exception {
dockerImagesBuilder =
new ParallelDockerImagesBuilder(
identity, false, 10, dockerCredentials, dockerConnector, machineLoggersFactory);
}
@Test(
expectedExceptions = InfrastructureException.class,
expectedExceptionsMessageRegExp =
"Che container '.*' doesn't have neither build nor image fields"
)
void shouldThrowExceptionWhenNoBuildDataPresent() throws Throwable {
DockerContainerConfig config = new DockerContainerConfig();
config.setBuild(new DockerBuildContext());
Map<String, DockerContainerConfig> input = singletonMap("machine1", config);
dockerImagesBuilder.prepareImages(input);
}
@Test(
expectedExceptions = InfrastructureException.class,
expectedExceptionsMessageRegExp =
"Try to build a docker machine source with an invalid location/content. It is not in the expected format"
)
void shouldThrowExceptionWhenImageFormatWrong() throws Throwable {
DockerContainerConfig config = new DockerContainerConfig();
config.setImage("**/%%");
Map<String, DockerContainerConfig> input = singletonMap("machine1", config);
dockerImagesBuilder.prepareImages(input);
}
@Test
void shouldPullAllImages() throws Throwable {
DockerContainerConfig config1 =
new DockerContainerConfig().setContainerName("container1").setImage("ubuntu/jdk8");
DockerContainerConfig config2 =
new DockerContainerConfig().setContainerName("container2").setImage("ubuntu/jdk9");
Map<String, DockerContainerConfig> input = new HashMap<>();
input.put("machine1", config1);
input.put("machine2", config2);
when(dockerConnector.listImages(any())).thenReturn(Collections.emptyList());
Map<String, String> result = dockerImagesBuilder.prepareImages(input);
verify(dockerConnector, times(input.size())).pull(any(), any());
verify(dockerConnector, times(input.size())).tag(any());
assertEquals(result.size(), input.size());
assertTrue(result.keySet().containsAll(input.keySet()));
assertTrue(result.values().contains("eclipse-che/" + config1.getContainerName()));
assertTrue(result.values().contains("eclipse-che/" + config2.getContainerName()));
}
@Test
void shouldBuildAllImages() throws Throwable {
Map<String, String> args1 = singletonMap("key1", "value1");
DockerBuildContext context1 =
new DockerBuildContext().setDockerfileContent("FROM ubuntu/jdk8").setArgs(args1);
DockerContainerConfig config1 =
new DockerContainerConfig().setBuild(context1).setMemLimit(1_024_000_000L);
Map<String, String> args2 = singletonMap("key2", "value2");
DockerBuildContext context2 =
new DockerBuildContext().setDockerfileContent("FROM ubuntu/jdk9").setArgs(args2);
DockerContainerConfig config2 =
new DockerContainerConfig().setBuild(context2).setMemLimit(2_048_000_000L);
Map<String, DockerContainerConfig> input = new HashMap<>();
input.put("machine1", config1);
input.put("machine2", config2);
when(dockerConnector.listImages(any())).thenReturn(Collections.emptyList());
Map<String, String> result = dockerImagesBuilder.prepareImages(input);
ArgumentCaptor<BuildImageParams> captor = ArgumentCaptor.forClass(BuildImageParams.class);
verify(dockerConnector, times(input.size())).buildImage(captor.capture(), any());
assertEquals(result.size(), input.size());
List<BuildImageParams> list = captor.getAllValues();
assertTrue(
list.stream()
.map(BuildImageParams::getMemoryLimit)
.anyMatch(l -> l.equals(config1.getMemLimit())));
assertTrue(
list.stream()
.map(BuildImageParams::getMemoryLimit)
.anyMatch(l -> l.equals(config2.getMemLimit())));
assertTrue(list.stream().map(BuildImageParams::getBuildArgs).anyMatch(m -> m.equals(args1)));
assertTrue(list.stream().map(BuildImageParams::getBuildArgs).anyMatch(m -> m.equals(args2)));
}
}