/******************************************************************************* * 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.inject.assistedinject.Assisted; import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.core.model.machine.Command; import org.eclipse.che.api.core.model.machine.MachineMetadata; import org.eclipse.che.api.core.model.machine.MachineState; import org.eclipse.che.api.core.util.LineConsumer; import org.eclipse.che.api.core.util.ListLineConsumer; import org.eclipse.che.api.machine.server.exception.MachineException; import org.eclipse.che.api.machine.server.impl.AbstractInstance; 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.InstanceProcess; import org.eclipse.che.commons.lang.NameGenerator; import org.eclipse.che.plugin.docker.client.DockerConnector; import org.eclipse.che.plugin.docker.client.Exec; import org.eclipse.che.plugin.docker.client.LogMessage; import org.eclipse.che.plugin.docker.client.ProgressLineFormatterImpl; import org.eclipse.che.plugin.docker.client.json.ContainerInfo; import org.eclipse.che.plugin.docker.machine.node.DockerNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; import java.io.IOException; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; import static java.lang.String.format; /** * Docker implementation of {@link Instance} * * @author andrew00x * @author Alexander Garagatyi * @author Anton Korneta */ public class DockerInstance extends AbstractInstance { private static final Logger LOG = LoggerFactory.getLogger(DockerInstance.class); private static final AtomicInteger pidSequence = new AtomicInteger(1); private static final String PID_FILE_TEMPLATE = "/tmp/docker-exec-%s.pid"; private static final Pattern PID_FILE_PATH_PATTERN = Pattern.compile(String.format(PID_FILE_TEMPLATE, "([0-9]+)")); /** * Produces output in form: * <pre> * /some/path/pid_file_template-1.pid * /some/path/pid_file_template-3.pid * /some/path/pid_file_template-14.pid * </pre> * Where each line is full path to pid file of <b>process that is running<b/> */ private static final String GET_ALIVE_PROCESSES_COMMAND = format("for pidFile in $(find %s -print 2>/dev/null); do kill -0 \"$(cat ${pidFile})\" 2>/dev/null && echo \"${pidFile}\"; done", format(PID_FILE_TEMPLATE, "*")); private final DockerMachineFactory dockerMachineFactory; private final String container; private final DockerConnector docker; private final String image; private final LineConsumer outputConsumer; private final String registry; private final DockerNode node; private final DockerInstanceStopDetector dockerInstanceStopDetector; private final DockerInstanceProcessesCleaner processesCleaner; private final ConcurrentHashMap<Integer, InstanceProcess> machineProcesses; private DockerInstanceMetadata machineMetadata; @Inject public DockerInstance(DockerConnector docker, @Named("machine.docker.registry") String registry, DockerMachineFactory dockerMachineFactory, @Assisted MachineState machineState, @Assisted("container") String container, @Assisted("image") String image, @Assisted DockerNode node, @Assisted LineConsumer outputConsumer, DockerInstanceStopDetector dockerInstanceStopDetector, DockerInstanceProcessesCleaner processesCleaner) { super(machineState); this.dockerMachineFactory = dockerMachineFactory; this.container = container; this.docker = docker; this.image = image; this.outputConsumer = outputConsumer; this.registry = registry; this.node = node; this.dockerInstanceStopDetector = dockerInstanceStopDetector; this.processesCleaner = processesCleaner; this.machineProcesses = new ConcurrentHashMap<>(); processesCleaner.trackProcesses(this); } @Override public LineConsumer getLogger() { return outputConsumer; } @Override public MachineMetadata getMetadata() { try { if (machineMetadata == null) { final ContainerInfo containerInfo = docker.inspectContainer(container); machineMetadata = dockerMachineFactory.createMetadata(containerInfo, node.getHost()); } return machineMetadata; } catch (IOException e) { LOG.error(e.getLocalizedMessage(), e); return null; } } @Override public InstanceProcess getProcess(final int pid) throws NotFoundException, MachineException { final InstanceProcess machineProcess = machineProcesses.get(pid); if (machineProcess != null) { try { machineProcess.checkAlive(); return machineProcess; } catch (NotFoundException e) { machineProcesses.remove(pid); throw e; } } throw new NotFoundException(format("Process with pid %s not found", pid)); } @Override public List<InstanceProcess> getProcesses() throws MachineException { List<InstanceProcess> processes = new LinkedList<>(); try { final Exec exec = docker.createExec(container, false, "/bin/bash", "-c", GET_ALIVE_PROCESSES_COMMAND); docker.startExec(exec.getId(), logMessage -> { final String pidFilePath = logMessage.getContent().trim(); final Matcher matcher = PID_FILE_PATH_PATTERN.matcher(pidFilePath); if (matcher.matches()) { final int virtualPid = Integer.parseInt(matcher.group(1)); final InstanceProcess dockerProcess = machineProcesses.get(virtualPid); if (dockerProcess != null) { processes.add(dockerProcess); } else { LOG.warn("Machine process {} exists in container but missing in processes map", virtualPid); } } }); return processes; } catch (IOException e) { throw new MachineException(e); } } @Override public InstanceProcess createProcess(Command command, String outputChannel) throws MachineException { final Integer pid = pidSequence.getAndIncrement(); final InstanceProcess process = dockerMachineFactory.createProcess(command, container, outputChannel, String.format(PID_FILE_TEMPLATE, pid), pid); machineProcesses.put(pid, process); return process; } @Override public InstanceKey saveToSnapshot(String owner) throws MachineException { try { final String repository = generateRepository(); String comment = format("Suspended at %1$ta %1$tb %1$td %1$tT %1$tZ %1$tY", System.currentTimeMillis()); if (owner != null) { comment = comment + " by " + owner; } // !! We SHOULD NOT pause container before commit because all execs will fail // to push image to private registry it should be tagged with registry in repo name // https://docs.docker.com/reference/api/docker_remote_api_v1.16/#push-an-image-on-the-registry docker.commit(container, registry + "/" + repository, null, comment, owner); //TODO fix this workaround. Docker image is not visible after commit when using swarm Thread.sleep(2000); final ProgressLineFormatterImpl progressLineFormatter = new ProgressLineFormatterImpl(); docker.push(repository, null, registry, currentProgressStatus -> { try { outputConsumer.writeLine(progressLineFormatter.format(currentProgressStatus)); } catch (IOException ignored) { } }); return new DockerInstanceKey(repository, registry); } catch (IOException e) { throw new MachineException(e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new MachineException(e.getLocalizedMessage(), e); } } private String generateRepository() { return NameGenerator.generate(null, 16); } @Override public void destroy() throws MachineException { machineProcesses.clear(); processesCleaner.untrackProcesses(getId()); dockerInstanceStopDetector.stopDetection(container); try { if (isDev()) { node.unbindWorkspace(); } docker.killContainer(container); docker.removeContainer(container, true, true); } catch (IOException e) { throw new MachineException(e.getLocalizedMessage()); } try { docker.removeImage(image, false); } catch (IOException ignore) { } } @Override public DockerNode getNode() { return node; } /** * Reads file content by specified file path. * * TODO: * add file size checking, * note that size checking and file content reading * should be done in an atomic way, * which means that two separate instance processes is not the case. * * @param filePath * path to file on machine instance * @param startFrom * line number to start reading from * @param limit * limitation on line * @return if {@code limit} and {@code startFrom} grater than 0 * content from {@code startFrom} to {@code startFrom + limit} will be returned, * if file contains less lines than {@code startFrom} empty content will be returned * @throws MachineException * if any error occurs with file reading */ @Override public String readFileContent(String filePath, int startFrom, int limit) throws MachineException { if (limit <= 0 || startFrom <= 0) { throw new MachineException("Impossible to read file " + limit + " lines from " + startFrom + " line"); } // command sed getting file content from startFrom line to (startFrom + limit) String bashCommand = format("sed -n \'%1$2s, %2$2sp\' %3$2s", startFrom, startFrom + limit, filePath); final String[] command = {"/bin/bash", "-c", bashCommand}; ListLineConsumer lines = new ListLineConsumer(); try { Exec exec = docker.createExec(container, false, command); docker.startExec(exec.getId(), new LogMessagePrinter(lines, LogMessage::getContent)); } catch (IOException e) { throw new MachineException(format("Error occurs while initializing command %s in docker container %s: %s", Arrays.toString(command), container, e.getLocalizedMessage()), e); } String content = lines.getText(); if (content.contains("sed: can't read " + filePath + ": No such file or directory") || content.contains("cat: " + filePath + ": No such file or directory")) { throw new MachineException("File with path " + filePath + " not found"); } return content; } /** * Copies files from specified container. * * @param sourceMachine * source machine * @param sourcePath * path to file or directory inside specified container * @param targetPath * path to destination file or directory inside container * @param overwrite * If "false" then it will be an error if unpacking the given content would cause * an existing directory to be replaced with a non-directory and vice versa. * @throws MachineException * if any error occurs when files are being copied */ @Override public void copy(Instance sourceMachine, String sourcePath, String targetPath, boolean overwrite) throws MachineException { if (!(sourceMachine instanceof DockerInstance)) { throw new MachineException("Unsupported copying between not docker machines"); } try { docker.putResource(container, targetPath, docker.getResource(((DockerInstance)sourceMachine).container, sourcePath), overwrite); } catch (IOException e) { throw new MachineException(e.getLocalizedMessage()); } } /** * Removes process from the list of processes * * <p>Used by {@link DockerInstanceProcessesCleaner} */ void removeProcess(int pid) { machineProcesses.remove(pid); } }