/******************************************************************************* * Copyright (c) 2012-2016 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.base.Strings; import com.google.common.collect.Maps; import com.google.common.collect.ObjectArrays; import com.google.common.collect.Sets; import com.google.inject.Inject; import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.core.model.machine.MachineState; import org.eclipse.che.api.core.model.machine.Recipe; import org.eclipse.che.api.core.util.FileCleaner; import org.eclipse.che.api.core.util.LineConsumer; import org.eclipse.che.api.core.util.SystemInfo; import org.eclipse.che.api.machine.server.exception.InvalidRecipeException; import org.eclipse.che.api.machine.server.exception.MachineException; import org.eclipse.che.api.machine.server.exception.SnapshotException; import org.eclipse.che.api.machine.server.spi.Instance; import org.eclipse.che.api.machine.server.spi.InstanceKey; import org.eclipse.che.api.machine.server.spi.InstanceProvider; import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.commons.env.EnvironmentContext; import org.eclipse.che.commons.lang.IoUtil; import org.eclipse.che.commons.lang.NameGenerator; import org.eclipse.che.plugin.docker.client.DockerConnector; import org.eclipse.che.plugin.docker.client.DockerConnectorConfiguration; import org.eclipse.che.plugin.docker.client.DockerFileException; import org.eclipse.che.plugin.docker.client.Dockerfile; import org.eclipse.che.plugin.docker.client.DockerfileParser; import org.eclipse.che.plugin.docker.client.ProgressLineFormatterImpl; import org.eclipse.che.plugin.docker.client.ProgressMonitor; import org.eclipse.che.plugin.docker.client.json.ContainerConfig; import org.eclipse.che.plugin.docker.client.json.HostConfig; import org.eclipse.che.plugin.docker.machine.node.DockerNode; import org.eclipse.che.plugin.docker.machine.node.WorkspaceFolderPathProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Named; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.file.Files; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import static com.google.common.base.Strings.isNullOrEmpty; /** * Docker implementation of {@link InstanceProvider} * * @author andrew00x * @author Alexander Garagatyi */ public class DockerInstanceProvider implements InstanceProvider { private static final Logger LOG = LoggerFactory.getLogger(DockerInstanceProvider.class); private final DockerConnector docker; private final DockerInstanceStopDetector dockerInstanceStopDetector; private final WorkspaceFolderPathProvider workspaceFolderPathProvider; private final boolean doForcePullOnBuild; private final Set<String> supportedRecipeTypes; private final DockerMachineFactory dockerMachineFactory; private final Map<String, String> devMachineContainerLabels; private final Map<String, String> commonMachineContainerLabels; private final Map<String, Map<String, String>> devMachinePortsToExpose; private final Map<String, Map<String, String>> commonMachinePortsToExpose; private final String[] devMachineSystemVolumes; private final String[] commonMachineSystemVolumes; private final String[] devMachineEnvVariables; private final String[] commonMachineEnvVariables; private final String[] allMachinesExtraHosts; private final String projectFolderPath; @Inject public DockerInstanceProvider(DockerConnector docker, DockerConnectorConfiguration dockerConnectorConfiguration, DockerMachineFactory dockerMachineFactory, DockerInstanceStopDetector dockerInstanceStopDetector, @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, @Nullable @Named("machine.docker.machine_extra_hosts") String allMachinesExtraHosts, WorkspaceFolderPathProvider workspaceFolderPathProvider, @Named("che.machine.projects.internal.storage") String projectFolderPath, @Named("machine.docker.pull_image") boolean doForcePullOnBuild, @Named("machine.docker.dev_machine.machine_env") Set<String> devMachineEnvVariables, @Named("machine.docker.machine_env") Set<String> allMachinesEnvVariables) throws IOException { this.docker = docker; this.dockerMachineFactory = dockerMachineFactory; this.dockerInstanceStopDetector = dockerInstanceStopDetector; this.workspaceFolderPathProvider = workspaceFolderPathProvider; this.doForcePullOnBuild = doForcePullOnBuild; this.supportedRecipeTypes = Collections.singleton("Dockerfile"); this.projectFolderPath = projectFolderPath; if (SystemInfo.isWindows()) { allMachinesSystemVolumes = escapePaths(allMachinesSystemVolumes); devMachineSystemVolumes = escapePaths(devMachineSystemVolumes); } this.commonMachineSystemVolumes = allMachinesSystemVolumes.toArray(new String[allMachinesEnvVariables.size()]); final Set<String> devMachineVolumes = Sets.newHashSetWithExpectedSize(allMachinesSystemVolumes.size() + devMachineSystemVolumes.size()); devMachineVolumes.addAll(allMachinesSystemVolumes); devMachineVolumes.addAll(devMachineSystemVolumes); this.devMachineSystemVolumes = devMachineVolumes.toArray(new String[devMachineVolumes.size()]); this.devMachinePortsToExpose = Maps.newHashMapWithExpectedSize(allMachinesServers.size() + devMachineServers.size()); this.commonMachinePortsToExpose = Maps.newHashMapWithExpectedSize(allMachinesServers.size()); this.devMachineContainerLabels = Maps.newHashMapWithExpectedSize(2 * allMachinesServers.size() + 2 * devMachineServers.size()); this.commonMachineContainerLabels = Maps.newHashMapWithExpectedSize(2 * allMachinesServers.size()); for (ServerConf serverConf : devMachineServers) { devMachinePortsToExpose.put(serverConf.getPort(), Collections.<String, String>emptyMap()); devMachineContainerLabels.put("che:server:" + serverConf.getPort() + ":ref", serverConf.getRef()); devMachineContainerLabels.put("che:server:" + serverConf.getPort() + ":protocol", serverConf.getProtocol()); } for (ServerConf serverConf : allMachinesServers) { commonMachinePortsToExpose.put(serverConf.getPort(), Collections.<String, String>emptyMap()); devMachinePortsToExpose.put(serverConf.getPort(), Collections.<String, String>emptyMap()); commonMachineContainerLabels.put("che:server:" + serverConf.getPort() + ":ref", serverConf.getRef()); devMachineContainerLabels.put("che:server:" + serverConf.getPort() + ":ref", serverConf.getRef()); commonMachineContainerLabels.put("che:server:" + serverConf.getPort() + ":protocol", serverConf.getProtocol()); devMachineContainerLabels.put("che:server:" + serverConf.getPort() + ":protocol", serverConf.getProtocol()); } allMachinesEnvVariables = filterEmptyAndNullValues(allMachinesEnvVariables); devMachineEnvVariables = filterEmptyAndNullValues(devMachineEnvVariables); this.commonMachineEnvVariables = allMachinesEnvVariables.toArray(new String[allMachinesEnvVariables.size()]); final HashSet<String> envVariablesForDevMachine = Sets.newHashSetWithExpectedSize(allMachinesEnvVariables.size() + devMachineEnvVariables.size()); envVariablesForDevMachine.addAll(allMachinesEnvVariables); envVariablesForDevMachine.addAll(devMachineEnvVariables); this.devMachineEnvVariables = envVariablesForDevMachine.toArray(new String[envVariablesForDevMachine.size()]); // always add the docker host String dockerHost = DockerInstanceMetadata.CHE_HOST.concat(":").concat(dockerConnectorConfiguration.getDockerHostIp()); if (isNullOrEmpty(allMachinesExtraHosts)) { this.allMachinesExtraHosts = new String[] {dockerHost}; } else { this.allMachinesExtraHosts = ObjectArrays.concat(allMachinesExtraHosts.split(","), dockerHost); } } /** * 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 */ protected Set<String> escapePaths(Set<String> paths) { return paths.stream().map(this::escapePath).collect(Collectors.toSet()); } /** * Escape path for Windows system with boot@docker according to rules given here : * https://github.com/boot2docker/boot2docker/blob/master/README.md#virtualbox-guest-additions * * @param path * path to escape * @return escaped path */ protected String escapePath(String path) { String esc; if (path.indexOf(":") == 1) { //check and replace only occurrence of ":" after disk label on Windows host (e.g. C:/) // but keep other occurrences it can be marker for docker mount volumes // (e.g. /path/dir/from/host:/name/of/dir/in/container ) esc = path.replaceFirst(":", "").replace('\\', '/'); esc = Character.toLowerCase(esc.charAt(0)) + esc.substring(1); //letter of disk mark must be lower case } else { esc = path.replace('\\', '/'); } if (!esc.startsWith("/")) { esc = "/" + esc; } return esc; } @Override public String getType() { return "docker"; } @Override public Set<String> getRecipeTypes() { return supportedRecipeTypes; } @Override public Instance createInstance(Recipe recipe, MachineState machineState, LineConsumer creationLogsOutput) throws MachineException { final Dockerfile dockerfile = parseRecipe(recipe); final String machineContainerName = generateContainerName(machineState.getWorkspaceId(), machineState.getName()); final String machineImageName = "eclipse-che/" + machineContainerName; buildImage(dockerfile, creationLogsOutput, machineImageName); return createInstance(machineContainerName, machineState, machineImageName, creationLogsOutput); } @Override public Instance createInstance(InstanceKey instanceKey, MachineState machineState, LineConsumer creationLogsOutput) throws NotFoundException, MachineException { final DockerInstanceKey dockerInstanceKey = new DockerInstanceKey(instanceKey); pullImage(dockerInstanceKey, creationLogsOutput); final String machineContainerName = generateContainerName(machineState.getWorkspaceId(), machineState.getName()); final String machineImageName = "eclipse-che/" + machineContainerName; final String fullNameOfPulledImage = dockerInstanceKey.getFullName(); try { // tag image with generated name to allow sysadmin recognize it docker.tag(fullNameOfPulledImage, machineImageName, null); } catch (IOException e) { LOG.error(e.getLocalizedMessage(), e); throw new MachineException("Can't create machine from snapshot."); } try { // remove unneeded tag docker.removeImage(fullNameOfPulledImage, false); } catch (IOException e) { LOG.error(e.getLocalizedMessage(), e); } return createInstance(machineContainerName, machineState, machineImageName, creationLogsOutput); } private Dockerfile parseRecipe(Recipe recipe) throws InvalidRecipeException { final Dockerfile dockerfile = getDockerFile(recipe); if (dockerfile.getImages().isEmpty()) { throw new InvalidRecipeException("Unable build docker based machine, Dockerfile found but it doesn't contain base image."); } return dockerfile; } private Dockerfile getDockerFile(Recipe recipe) throws InvalidRecipeException { if (recipe.getScript() == null) { throw new InvalidRecipeException("Unable build docker based machine, recipe isn't set or doesn't provide Dockerfile and " + "no Dockerfile found in the list of files attached to this builder."); } try { return DockerfileParser.parse(recipe.getScript()); } catch (DockerFileException e) { LOG.debug(e.getLocalizedMessage(), e); throw new InvalidRecipeException(String.format("Unable build docker based machine. %s", e.getMessage())); } } private void buildImage(Dockerfile dockerfile, final LineConsumer creationLogsOutput, String imageName) throws MachineException { File workDir = null; try { // build docker image workDir = Files.createTempDirectory(null).toFile(); final File dockerfileFile = new File(workDir, "Dockerfile"); dockerfile.writeDockerfile(dockerfileFile); final List<File> files = new LinkedList<>(); //noinspection ConstantConditions Collections.addAll(files, workDir.listFiles()); final ProgressLineFormatterImpl progressLineFormatter = new ProgressLineFormatterImpl(); final ProgressMonitor progressMonitor = currentProgressStatus -> { try { creationLogsOutput.writeLine(progressLineFormatter.format(currentProgressStatus)); } catch (IOException e) { LOG.error(e.getLocalizedMessage(), e); } }; docker.buildImage(imageName, progressMonitor, null, doForcePullOnBuild, files.toArray(new File[files.size()])); } catch (IOException | InterruptedException e) { throw new MachineException(e.getMessage(), e); } finally { if (workDir != null) { FileCleaner.addFile(workDir); } } } private void pullImage(DockerInstanceKey dockerInstanceKey, final LineConsumer creationLogsOutput) throws MachineException { if (dockerInstanceKey.getRepository() == null) { throw new MachineException("Machine creation failed. Snapshot state is invalid. Please, contact support."); } try { final ProgressLineFormatterImpl progressLineFormatter = new ProgressLineFormatterImpl(); docker.pull(dockerInstanceKey.getRepository(), dockerInstanceKey.getTag(), dockerInstanceKey.getRegistry(), currentProgressStatus -> { try { creationLogsOutput.writeLine(progressLineFormatter.format(currentProgressStatus)); } catch (IOException e) { LOG.error(e.getLocalizedMessage(), e); } }); } catch (IOException | InterruptedException e) { throw new MachineException(e.getLocalizedMessage(), e); } } // TODO rework in accordance with v2 docker registry API @Override public void removeInstanceSnapshot(InstanceKey instanceKey) throws SnapshotException { // use registry API directly because docker doesn't have such API yet // https://github.com/docker/docker-registry/issues/45 final DockerInstanceKey dockerInstanceKey = new DockerInstanceKey(instanceKey); String registry = dockerInstanceKey.getRegistry(); String repository = dockerInstanceKey.getRepository(); if (registry == null || repository == null) { throw new SnapshotException("Snapshot removing failed. Snapshot attributes are not valid"); } StringBuilder sb = new StringBuilder("http://");// TODO make possible to use https here sb.append(registry).append("/v1/repositories/"); sb.append(repository); sb.append("/");// do not remove! Doesn't work without this slash try { final HttpURLConnection conn = (HttpURLConnection)new URL(sb.toString()).openConnection(); try { conn.setConnectTimeout(30 * 1000); conn.setRequestMethod("DELETE"); // fixme add auth header for secured registry // conn.setRequestProperty("Authorization", authHeader); final int responseCode = conn.getResponseCode(); if ((responseCode / 100) != 2) { InputStream in = conn.getErrorStream(); if (in == null) { in = conn.getInputStream(); } LOG.error(IoUtil.readAndCloseQuietly(in)); throw new SnapshotException("Internal server error occurs. Can't remove snapshot"); } } finally { conn.disconnect(); } } catch (IOException e) { LOG.error(e.getLocalizedMessage(), e); } } private Instance createInstance(String containerName, MachineState machineState, String imageName, LineConsumer outputConsumer) throws MachineException { try { final Map<String, String> labels; final Map<String, Map<String, String>> portsToExpose; final String[] volumes; final String[] env; if (machineState.isDev()) { labels = devMachineContainerLabels; portsToExpose = devMachinePortsToExpose; final String projectFolderVolume = String.format("%s:%s", workspaceFolderPathProvider.getPath(machineState.getWorkspaceId()), projectFolderPath); volumes = ObjectArrays.concat(devMachineSystemVolumes, SystemInfo.isWindows() ? escapePath(projectFolderVolume) : projectFolderVolume); String[] vars = {DockerInstanceMetadata.CHE_WORKSPACE_ID + '=' + machineState.getWorkspaceId(), DockerInstanceMetadata.USER_TOKEN + '=' + EnvironmentContext.getCurrent().getUser().getToken()}; env = ObjectArrays.concat(devMachineEnvVariables, vars, String.class); } else { labels = commonMachineContainerLabels; portsToExpose = commonMachinePortsToExpose; volumes = commonMachineSystemVolumes; env = commonMachineEnvVariables; } final HostConfig hostConfig = new HostConfig().withBinds(volumes) .withExtraHosts(allMachinesExtraHosts) .withPublishAllPorts(true) .withMemorySwap(-1) .withMemory((long)machineState.getLimits().getRam() * 1024 * 1024); final ContainerConfig config = new ContainerConfig().withImage(imageName) .withLabels(labels) .withExposedPorts(portsToExpose) .withHostConfig(hostConfig) .withEnv(env); final String containerId = docker.createContainer(config, containerName).getId(); docker.startContainer(containerId, null); final DockerNode node = dockerMachineFactory.createNode(machineState.getWorkspaceId(), containerId); if (machineState.isDev()) { node.bindWorkspace(); } dockerInstanceStopDetector.startDetection(containerId, machineState.getId()); return dockerMachineFactory.createInstance(machineState, containerId, imageName, node, outputConsumer); } catch (IOException e) { throw new MachineException(e); } } String generateContainerName(String workspaceId, String displayName) { String userName = EnvironmentContext.getCurrent().getUser().getName(); final String containerName = userName + '_' + workspaceId + '_' + displayName + '_'; // removing all not allowed characters + generating random name suffix return NameGenerator.generate(containerName.replaceAll("[^a-zA-Z0-9_-]+", ""), 5); } /** * Returns set that contains all non empty and non nullable values from specified set */ protected Set<String> filterEmptyAndNullValues(Set<String> paths) { return paths.stream() .filter(path -> !Strings.isNullOrEmpty(path)) .collect(Collectors.toSet()); } }