diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties index ba86b29afa..3805d881c3 100644 --- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties @@ -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 diff --git a/infrastructures/docker/infrastructure/src/main/java/org/eclipse/che/workspace/infrastructure/docker/DockerInfraModule.java b/infrastructures/docker/infrastructure/src/main/java/org/eclipse/che/workspace/infrastructure/docker/DockerInfraModule.java index 1cfd6ef11a..d4e741134b 100644 --- a/infrastructures/docker/infrastructure/src/main/java/org/eclipse/che/workspace/infrastructure/docker/DockerInfraModule.java +++ b/infrastructures/docker/infrastructure/src/main/java/org/eclipse/che/workspace/infrastructure/docker/DockerInfraModule.java @@ -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); diff --git a/infrastructures/docker/infrastructure/src/main/java/org/eclipse/che/workspace/infrastructure/docker/DockerInternalRuntime.java b/infrastructures/docker/infrastructure/src/main/java/org/eclipse/che/workspace/infrastructure/docker/DockerInternalRuntime.java index 57d622ff19..9f0d99550e 100644 --- a/infrastructures/docker/infrastructure/src/main/java/org/eclipse/che/workspace/infrastructure/docker/DockerInternalRuntime.java +++ b/infrastructures/docker/infrastructure/src/main/java/org/eclipse/che/workspace/infrastructure/docker/DockerInternalRuntime.java @@ -76,6 +76,7 @@ public class DockerInternalRuntime extends InternalRuntime 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 ServersCheckerFactory serverCheckerFactory, MachineLoggersFactory loggers, ProbeScheduler probeScheduler, - WorkspaceProbesFactory probesFactory) { + WorkspaceProbesFactory probesFactory, + ParallelDockerImagesBuilderFactory imagesBuilderFactory) { this( context, urlRewriter, @@ -106,7 +108,8 @@ public class DockerInternalRuntime extends InternalRuntime serverCheckerFactory, loggers, probeScheduler, - probesFactory); + probesFactory, + imagesBuilderFactory); } /** @@ -128,7 +131,8 @@ public class DockerInternalRuntime extends InternalRuntime 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 serverCheckerFactory, loggers, probeScheduler, - probesFactory); + probesFactory, + imagesBuilderFactory); for (ContainerListEntry container : containers) { DockerMachine machine = machineCreator.create(container); @@ -166,7 +171,8 @@ public class DockerInternalRuntime extends InternalRuntime 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 this.runtimeMachines = new RuntimeMachines(); this.loggers = loggers; this.probeScheduler = probeScheduler; + this.imagesBuilderFactory = imagesBuilderFactory; } @Override @@ -186,6 +193,10 @@ public class DockerInternalRuntime extends InternalRuntime startSynchronizer.setStartThread(); try { networks.createNetwork(getContext().getEnvironment().getNetwork()); + Map images = + imagesBuilderFactory + .create(getContext().getIdentity()) + .prepareImages(getContext().getEnvironment().getContainers()); for (Map.Entry containerEntry : getContext().getEnvironment().getContainers().entrySet()) { @@ -196,7 +207,8 @@ public class DockerInternalRuntime extends InternalRuntime 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 } } - 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 containerStarter.startContainer( getContext().getEnvironment().getNetwork(), name, + image, containerConfig, identity, new AbnormalMachineStopHandlerImpl()); diff --git a/infrastructures/docker/infrastructure/src/main/java/org/eclipse/che/workspace/infrastructure/docker/DockerMachineStarter.java b/infrastructures/docker/infrastructure/src/main/java/org/eclipse/che/workspace/infrastructure/docker/DockerMachineStarter.java index 43f0761089..f1353ab853 100644 --- a/infrastructures/docker/infrastructure/src/main/java/org/eclipse/che/workspace/infrastructure/docker/DockerMachineStarter.java +++ b/infrastructures/docker/infrastructure/src/main/java/org/eclipse/che/workspace/infrastructure/docker/DockerMachineStarter.java @@ -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 { diff --git a/infrastructures/docker/infrastructure/src/main/java/org/eclipse/che/workspace/infrastructure/docker/ParallelDockerImagesBuilder.java b/infrastructures/docker/infrastructure/src/main/java/org/eclipse/che/workspace/infrastructure/docker/ParallelDockerImagesBuilder.java new file mode 100644 index 0000000000..ce24853011 --- /dev/null +++ b/infrastructures/docker/infrastructure/src/main/java/org/eclipse/che/workspace/infrastructure/docker/ParallelDockerImagesBuilder.java @@ -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 prepareImages(Map 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 machineToImageNames = new ConcurrentHashMap<>(containers.size()); + CompletableFuture firstFailed = new CompletableFuture<>(); + List> taskFutures = + containers + .entrySet() + .stream() + .map( + e -> + CompletableFuture.supplyAsync( + (Supplier) + () -> { + 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 + } + } +} diff --git a/infrastructures/docker/infrastructure/src/main/java/org/eclipse/che/workspace/infrastructure/docker/ParallelDockerImagesBuilderFactory.java b/infrastructures/docker/infrastructure/src/main/java/org/eclipse/che/workspace/infrastructure/docker/ParallelDockerImagesBuilderFactory.java new file mode 100644 index 0000000000..de4a73ea1a --- /dev/null +++ b/infrastructures/docker/infrastructure/src/main/java/org/eclipse/che/workspace/infrastructure/docker/ParallelDockerImagesBuilderFactory.java @@ -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; +} diff --git a/infrastructures/docker/infrastructure/src/test/java/org/eclipse/che/workspace/infrastructure/docker/DockerInternalRuntimeTest.java b/infrastructures/docker/infrastructure/src/test/java/org/eclipse/che/workspace/infrastructure/docker/DockerInternalRuntimeTest.java index 03e02bca53..429f2a33e8 100644 --- a/infrastructures/docker/infrastructure/src/test/java/org/eclipse/che/workspace/infrastructure/docker/DockerInternalRuntimeTest.java +++ b/infrastructures/docker/infrastructure/src/test/java/org/eclipse/che/workspace/infrastructure/docker/DockerInternalRuntimeTest.java @@ -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> probeResultConsumerCaptor; @Captor private ArgumentCaptor 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), diff --git a/infrastructures/docker/infrastructure/src/test/java/org/eclipse/che/workspace/infrastructure/docker/ParallelDockerImagesBuilderTest.java b/infrastructures/docker/infrastructure/src/test/java/org/eclipse/che/workspace/infrastructure/docker/ParallelDockerImagesBuilderTest.java new file mode 100644 index 0000000000..099e984a50 --- /dev/null +++ b/infrastructures/docker/infrastructure/src/test/java/org/eclipse/che/workspace/infrastructure/docker/ParallelDockerImagesBuilderTest.java @@ -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 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 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 input = new HashMap<>(); + input.put("machine1", config1); + input.put("machine2", config2); + when(dockerConnector.listImages(any())).thenReturn(Collections.emptyList()); + Map 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 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 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 input = new HashMap<>(); + input.put("machine1", config1); + input.put("machine2", config2); + + when(dockerConnector.listImages(any())).thenReturn(Collections.emptyList()); + Map result = dockerImagesBuilder.prepareImages(input); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BuildImageParams.class); + verify(dockerConnector, times(input.size())).buildImage(captor.capture(), any()); + assertEquals(result.size(), input.size()); + List 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))); + } +}