/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.plugin.docker.machine; import com.google.common.annotations.VisibleForTesting; import com.google.inject.assistedinject.Assisted; import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.core.ServerException; import org.eclipse.che.api.core.model.machine.Command; import org.eclipse.che.api.core.model.machine.Machine; import org.eclipse.che.api.core.model.machine.MachineSource; 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.model.impl.MachineRuntimeInfoImpl; import org.eclipse.che.api.machine.server.spi.Instance; import org.eclipse.che.api.machine.server.spi.InstanceProcess; import org.eclipse.che.api.machine.server.spi.impl.AbstractInstance; import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.commons.lang.NameGenerator; import org.eclipse.che.plugin.docker.client.DockerConnector; import org.eclipse.che.plugin.docker.client.DockerConnectorProvider; 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.params.CommitParams; import org.eclipse.che.plugin.docker.client.params.CreateExecParams; import org.eclipse.che.plugin.docker.client.params.GetResourceParams; import org.eclipse.che.plugin.docker.client.params.PushParams; import org.eclipse.che.plugin.docker.client.params.PutResourceParams; import org.eclipse.che.plugin.docker.client.params.RemoveContainerParams; import org.eclipse.che.plugin.docker.client.params.RemoveImageParams; import org.eclipse.che.plugin.docker.client.params.StartExecParams; 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; import static org.eclipse.che.plugin.docker.machine.DockerInstanceProvider.MACHINE_SNAPSHOT_PREFIX; /** * Docker implementation of {@link Instance} * * @author andrew00x * @author Alexander Garagatyi * @author Anton Korneta * @author Mykola Morhun */ public class DockerInstance extends AbstractInstance { private static final Logger LOG = LoggerFactory.getLogger(DockerInstance.class); /** * Name of the latest tag used in Docker image. */ public static final String LATEST_TAG = "latest"; 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 String registryNamespace; private final DockerNode node; private final DockerInstanceStopDetector dockerInstanceStopDetector; private final DockerInstanceProcessesCleaner processesCleaner; private final ConcurrentHashMap<Integer, InstanceProcess> machineProcesses; private final boolean snapshotUseRegistry; private final MachineRuntimeInfoImpl machineRuntime; @Inject public DockerInstance(DockerConnectorProvider dockerProvider, @Named("che.docker.registry") String registry, @Named("che.docker.namespace") @Nullable String registryNamespace, DockerMachineFactory dockerMachineFactory, @Assisted Machine machine, @Assisted("container") String container, @Assisted("image") String image, @Assisted DockerNode node, @Assisted LineConsumer outputConsumer, DockerInstanceStopDetector dockerInstanceStopDetector, DockerInstanceProcessesCleaner processesCleaner, @Named("che.docker.registry_for_snapshots") boolean snapshotUseRegistry) throws MachineException { super(machine); this.dockerMachineFactory = dockerMachineFactory; this.container = container; this.docker = dockerProvider.get(); this.image = image; this.outputConsumer = outputConsumer; this.registry = registry; this.registryNamespace = registryNamespace; this.node = node; this.dockerInstanceStopDetector = dockerInstanceStopDetector; this.processesCleaner = processesCleaner; this.machineProcesses = new ConcurrentHashMap<>(); processesCleaner.trackProcesses(this); this.snapshotUseRegistry = snapshotUseRegistry; this.machineRuntime = doGetRuntime(); } @Override public LineConsumer getLogger() { return outputConsumer; } @Override public MachineRuntimeInfoImpl getRuntime() { return machineRuntime; } @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(CreateExecParams.create(container, new String[] {"/bin/sh", "-c", GET_ALIVE_PROCESSES_COMMAND}) .withDetach(false)); docker.startExec(StartExecParams.create(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 MachineSource saveToSnapshot() throws MachineException { try { String image = generateRepository(); if(!snapshotUseRegistry) { commitContainer(image, LATEST_TAG); return new DockerMachineSource(image).withTag(LATEST_TAG); } PushParams pushParams = PushParams.create(image) .withRegistry(registry) .withTag(LATEST_TAG); final String fullRepo = pushParams.getFullRepo(); commitContainer(fullRepo, LATEST_TAG); //TODO fix this workaround. Docker image is not visible after commit when using swarm Thread.sleep(2000); final ProgressLineFormatterImpl lineFormatter = new ProgressLineFormatterImpl(); final String digest = docker.push(pushParams, progressMonitor -> { try { outputConsumer.writeLine(lineFormatter.format(progressMonitor)); } catch (IOException ignored) { } }); docker.removeImage(RemoveImageParams.create(fullRepo).withForce(false)); return new DockerMachineSource(image).withRegistry(registry).withDigest(digest).withTag(LATEST_TAG); } catch (IOException ioEx) { throw new MachineException(ioEx); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new MachineException(e.getLocalizedMessage(), e); } } @VisibleForTesting protected void commitContainer(String repository, String tag) throws IOException { String comment = format("Suspended at %1$ta %1$tb %1$td %1$tT %1$tZ %1$tY", System.currentTimeMillis()); // !! 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(CommitParams.create(container) .withRepository(repository) .withTag(tag) .withComment(comment)); } private String generateRepository() { if (registryNamespace != null) { return registryNamespace + '/' + MACHINE_SNAPSHOT_PREFIX + NameGenerator.generate(null, 16); } return MACHINE_SNAPSHOT_PREFIX + NameGenerator.generate(null, 16); } @Override public void destroy() throws MachineException { try { outputConsumer.close(); } catch (IOException ignored) {} machineProcesses.clear(); processesCleaner.untrackProcesses(getId()); dockerInstanceStopDetector.stopDetection(container); try { if (getConfig().isDev()) { node.unbindWorkspace(); } // kill container is not needed here, because we removing container with force flag docker.removeContainer(RemoveContainerParams.create(container) .withRemoveVolumes(true) .withForce(true)); } catch (IOException | ServerException e) { LOG.error(e.getLocalizedMessage(), e); throw new MachineException(e.getLocalizedMessage()); } try { docker.removeImage(RemoveImageParams.create(image).withForce(false)); } catch (IOException ignore) { LOG.error("IOException during destroy(). Ignoring."); } } @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 shCommand = format("sed -n \'%1$2s, %2$2sp\' %3$2s", startFrom, startFrom + limit, filePath); final String[] command = {"/bin/sh", "-c", shCommand}; ListLineConsumer lines = new ListLineConsumer(); try { Exec exec = docker.createExec(CreateExecParams.create(container, command).withDetach(false)); docker.startExec(StartExecParams.create(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; } @Override public void copy(Instance sourceMachine, String sourcePath, String targetPath, boolean overwriteDirNonDir) throws MachineException { if (!(sourceMachine instanceof DockerInstance)) { throw new MachineException("Unsupported copying between not docker machines"); } try { docker.putResource(PutResourceParams.create(container, targetPath, docker.getResource(GetResourceParams.create( ((DockerInstance)sourceMachine).container, sourcePath))) .withNoOverwriteDirNonDir(overwriteDirNonDir)); } catch (IOException e) { throw new MachineException(e.getLocalizedMessage()); } } /** * Not implemented.<p/> * * {@inheritDoc} */ @Override public void copy(String sourcePath, String targetPath) throws MachineException { throw new MachineException("Unsupported operation for docker machine implementation"); } /** * Removes process from the list of processes * * <p>Used by {@link DockerInstanceProcessesCleaner} */ void removeProcess(int pid) { machineProcesses.remove(pid); } /** * Can be used for docker specific operations with machine */ public String getContainer() { return container; } private MachineRuntimeInfoImpl doGetRuntime() throws MachineException { try { return new MachineRuntimeInfoImpl(dockerMachineFactory.createMetadata(docker.inspectContainer(container), getConfig(), node.getHost())); } catch (IOException x) { throw new MachineException(x.getMessage(), x); } } }