blob: 192ff9708515a466fd10e187ce8464abf7a2f299 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015, 2016, 2017 Red Hat Inc. and others.
* 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 - Initial Contribution
*******************************************************************************/
package org.eclipse.linuxtools.docker.ui.launch;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.linuxtools.docker.core.DockerConnectionManager;
import org.eclipse.linuxtools.docker.core.DockerException;
import org.eclipse.linuxtools.docker.core.EnumDockerLoggingStatus;
import org.eclipse.linuxtools.docker.core.IDockerConnection;
import org.eclipse.linuxtools.docker.core.IDockerContainerConfig;
import org.eclipse.linuxtools.docker.core.IDockerContainerExit;
import org.eclipse.linuxtools.docker.core.IDockerContainerInfo;
import org.eclipse.linuxtools.docker.core.IDockerHostConfig;
import org.eclipse.linuxtools.docker.core.IDockerImage;
import org.eclipse.linuxtools.docker.core.IDockerImageInfo;
import org.eclipse.linuxtools.docker.core.IDockerPortBinding;
import org.eclipse.linuxtools.docker.ui.Activator;
import org.eclipse.linuxtools.internal.docker.core.DockerConnection;
import org.eclipse.linuxtools.internal.docker.core.DockerContainerConfig;
import org.eclipse.linuxtools.internal.docker.core.DockerHostConfig;
import org.eclipse.linuxtools.internal.docker.core.DockerPortBinding;
import org.eclipse.linuxtools.internal.docker.ui.consoles.ConsoleOutputStream;
import org.eclipse.linuxtools.internal.docker.ui.consoles.RunConsole;
import org.eclipse.linuxtools.internal.docker.ui.launch.ContainerCommandProcess;
import org.eclipse.linuxtools.internal.docker.ui.views.DVMessages;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PlatformUI;
public class ContainerLauncher {
private static final String ERROR_CREATING_CONTAINER = "ContainerCreateError.msg"; //$NON-NLS-1$
private static final String ERROR_LAUNCHING_CONTAINER = "ContainerLaunchError.msg"; //$NON-NLS-1$
private static final String ERROR_NO_CONNECTIONS = "ContainerNoConnections.msg"; //$NON-NLS-1$
private static final String ERROR_NO_CONNECTION_WITH_URI = "ContainerNoConnectionWithURI.msg"; //$NON-NLS-1$
private static final String DIRFILE_NAME = "copiedVolumes"; //$NON-NLS-1$
private static RunConsole console;
private static Map<IProject, ID> fidMap = new HashMap<>();
private static Object lockObject = new Object();
private static Map<String, Map<String, Set<String>>> copiedVolumesMap = null;
private class CopyVolumesJob extends Job {
private static final String COPY_VOLUMES_JOB_TITLE = "ContainerLaunch.copyVolumesJob.title"; //$NON-NLS-1$
private static final String COPY_VOLUMES_DESC = "ContainerLaunch.copyVolumesJob.desc"; //$NON-NLS-1$
private static final String COPY_VOLUMES_TASK = "ContainerLaunch.copyVolumesJob.task"; //$NON-NLS-1$
private static final String ERROR_COPYING_VOLUME = "ContainerLaunch.copyVolumesJob.error"; //$NON-NLS-1$
private final Set<String> volumes;
private final IDockerConnection connection;
private final String containerId;
public CopyVolumesJob(Set<String> volumes,
IDockerConnection connection,
String containerId) {
super(Messages.getString(COPY_VOLUMES_JOB_TITLE));
this.volumes = volumes;
this.connection = connection;
this.containerId = containerId;
}
@Override
protected IStatus run(IProgressMonitor monitor) {
monitor.beginTask(
Messages.getFormattedString(COPY_VOLUMES_DESC, containerId),
volumes.size());
Iterator<String> iterator = volumes.iterator();
IStatus status = Status.OK_STATUS;
// for each remote volume, copy from host to Container volume
while (iterator.hasNext()) {
if (monitor.isCanceled()) {
monitor.done();
return Status.CANCEL_STATUS;
}
String directory = iterator.next();
if (!directory.endsWith("/")) { //$NON-NLS-1$
directory = directory + "/"; //$NON-NLS-1$
}
monitor.setTaskName(Messages
.getFormattedString(COPY_VOLUMES_TASK, directory));
try {
((DockerConnection) connection).copyToContainer(directory,
containerId, directory);
monitor.worked(1);
} catch (DockerException | InterruptedException
| IOException e) {
monitor.done();
final String dir = directory;
Display.getDefault().syncExec(() -> MessageDialog.openError(
PlatformUI.getWorkbench().getActiveWorkbenchWindow()
.getShell(),
Messages.getFormattedString(ERROR_COPYING_VOLUME,
new String[] { dir, containerId }),
e.getMessage()));
status = new Status(IStatus.ERROR, Activator.PLUGIN_ID,
e.getMessage());
} finally {
monitor.done();
}
}
return status;
}
}
private class CopyVolumesFromImageJob extends Job {
private static final String COPY_VOLUMES_FROM_JOB_TITLE = "ContainerLaunch.copyVolumesFromJob.title"; //$NON-NLS-1$
private static final String COPY_VOLUMES_FROM_DESC = "ContainerLaunch.copyVolumesFromJob.desc"; //$NON-NLS-1$
private static final String COPY_VOLUMES_FROM_TASK = "ContainerLaunch.copyVolumesFromJob.task"; //$NON-NLS-1$
// private static final String ERROR_COPYING_VOLUME =
// "ContainerLaunch.copyVolumesFromJob.error"; //$NON-NLS-1$
private final List<String> volumes;
private final IDockerConnection connection;
private final String image;
private final IPath target;
private Set<String> dirList;
public CopyVolumesFromImageJob(
IDockerConnection connection,
String image, List<String> volumes, IPath target) {
super(Messages.getString(COPY_VOLUMES_FROM_JOB_TITLE));
this.volumes = volumes;
this.connection = connection;
this.image = image;
this.target = target;
Map<String, Set<String>> dirMap = null;
synchronized (lockObject) {
String uri = connection.getUri();
dirMap = copiedVolumesMap.get(uri);
if (dirMap == null) {
dirMap = new HashMap<>();
copiedVolumesMap.put(uri, dirMap);
}
dirList = dirMap.get(image);
if (dirList == null) {
dirList = new HashSet<>();
dirMap.put(image, dirList);
}
}
}
@Override
protected IStatus run(final IProgressMonitor monitor) {
monitor.beginTask(
Messages.getFormattedString(COPY_VOLUMES_FROM_DESC, image),
volumes.size());
String containerId = null;
try {
IDockerImage dockerImage = ((DockerConnection) connection)
.getImageByTag(image);
// if there is a .image_id file, check the image id to ensure
// the user hasn't loaded a new version which may have
// different header files installed.
IPath imageFilePath = target.append(".image_id"); //$NON-NLS-1$
File imageFile = imageFilePath.toFile();
boolean needImageIdFile = !imageFile.exists();
if (!needImageIdFile) {
try (FileReader reader = new FileReader(imageFile);
BufferedReader bufferReader = new BufferedReader(
reader);) {
String imageId = bufferReader.readLine();
if (!dockerImage.id().equals(imageId)) {
// if image id has changed...all bets are off
// and we must reload all directories
dirList.clear();
needImageIdFile = true;
}
} catch (IOException e) {
// ignore
}
}
if (needImageIdFile) {
try (FileWriter writer = new FileWriter(imageFile);
BufferedWriter bufferedWriter = new BufferedWriter(
writer);) {
bufferedWriter.write(dockerImage.id());
bufferedWriter.newLine();
} catch (IOException e) {
// ignore
}
}
DockerContainerConfig.Builder builder = new DockerContainerConfig.Builder()
.cmd("/bin/sh").image(image); //$NON-NLS-1$
IDockerContainerConfig config = builder.build();
DockerHostConfig.Builder hostBuilder = new DockerHostConfig.Builder();
IDockerHostConfig hostConfig = hostBuilder.build();
containerId = ((DockerConnection) connection)
.createContainer(config, hostConfig, null);
for (String volume : volumes) {
if (monitor.isCanceled()) {
monitor.done();
return Status.CANCEL_STATUS;
}
if (volume.contains("${ProjName}")) { //$NON-NLS-1$
monitor.worked(1);
continue;
}
// if we have already copied the directory either directly
// or as part of a parent directory copy, then skip to next
// volume.
for (String path : dirList) {
if (volume.equals(path)
|| (volume.startsWith(path) && volume.charAt(
path.length()) == File.separatorChar)) {
monitor.worked(1);
continue;
}
}
try {
monitor.setTaskName(Messages.getFormattedString(
COPY_VOLUMES_FROM_TASK, volume));
monitor.worked(1);
InputStream in = ((DockerConnection) connection)
.copyContainer(containerId, volume);
synchronized (lockObject) {
dirList.add(volume);
}
/*
* The input stream from copyContainer might be
* incomplete or non-blocking so we should wrap it in a
* stream that is guaranteed to block until data is
* available.
*/
TarArchiveInputStream k = new TarArchiveInputStream(
new BlockingInputStream(in));
TarArchiveEntry te = null;
target.toFile().mkdirs();
IPath currDir = target.append(volume)
.removeLastSegments(1);
currDir.toFile().mkdirs();
while ((te = k.getNextTarEntry()) != null) {
long size = te.getSize();
IPath path = currDir;
path = path.append(te.getName());
File f = new File(path.toOSString());
if (te.isDirectory()) {
f.mkdir();
continue;
} else {
f.createNewFile();
}
FileOutputStream os = new FileOutputStream(f);
int bufferSize = ((int) size > 4096 ? 4096
: (int) size);
byte[] barray = new byte[bufferSize];
int result = -1;
while ((result = k.read(barray, 0,
bufferSize)) > -1) {
if (monitor.isCanceled()) {
monitor.done();
k.close();
os.close();
return Status.CANCEL_STATUS;
}
os.write(barray, 0, result);
}
os.close();
}
k.close();
} catch (final DockerException e) {
// ignore
}
}
} catch (InterruptedException e) {
// do nothing
} catch (IOException e) {
Activator.log(e);
} catch (DockerException e1) {
Activator.log(e1);
} finally {
if (containerId != null) {
try {
((DockerConnection) connection)
.removeContainer(containerId);
} catch (DockerException | InterruptedException e) {
// ignore
}
}
monitor.done();
}
return Status.OK_STATUS;
}
}
/**
* A blocking input stream that waits until data is available.
*/
private class BlockingInputStream extends InputStream {
private InputStream in;
public BlockingInputStream(InputStream in) {
this.in = in;
}
@Override
public int read() throws IOException {
return in.read();
}
}
@Override
protected void finalize() throws Throwable {
synchronized (lockObject) {
if (copiedVolumesMap != null) {
IPath pluginPath = Platform.getStateLocation(
Platform.getBundle(Activator.PLUGIN_ID));
IPath path = pluginPath.append(DIRFILE_NAME);
File dirFile = path.toFile();
FileOutputStream f = new FileOutputStream(dirFile);
try (ObjectOutputStream oos = new ObjectOutputStream(f)) {
oos.writeObject(copiedVolumesMap);
}
}
}
super.finalize();
}
public ContainerLauncher() {
initialize();
}
@SuppressWarnings("unchecked")
private void initialize() {
synchronized (lockObject) {
if (copiedVolumesMap == null) {
IPath pluginPath = Platform.getStateLocation(
Platform.getBundle(Activator.PLUGIN_ID));
IPath path = pluginPath.append(DIRFILE_NAME);
File dirFile = path.toFile();
if (dirFile.exists()) {
try (FileInputStream f = new FileInputStream(dirFile)) {
try (ObjectInputStream ois = new ObjectInputStream(f)) {
copiedVolumesMap = (Map<String, Map<String, Set<String>>>) ois
.readObject();
} catch (ClassNotFoundException
| FileNotFoundException e) {
// should never happen
e.printStackTrace();
}
} catch (IOException e) {
// will handle this below
}
}
}
if (copiedVolumesMap == null) {
copiedVolumesMap = new HashMap<>();
}
}
}
/**
* Perform a launch of a command in a container.
*
* @param id
* - id of caller to use to distinguish console owner
* @param listener
* - optional listener of the run console
* @param connectionUri
* - the specified connection to use
* @param image
* - the image to use
* @param command
* - command to run
* @param commandDir
* - directory command requires or null
* @param workingDir
* - working directory or null
* @param additionalDirs
* - additional directories to mount or null
* @param origEnv
* - original environment if we are appending to our existing
* environment
* @param envMap
* - map of environment variable settings
* @param ports
* - ports to expose
* @param keep
* - keep container after running
* @param stdinSupport
* - true if stdin support is required, false otherwise
*/
public void launch(String id, IContainerLaunchListener listener,
final String connectionUri,
String image, String command, String commandDir, String workingDir,
List<String> additionalDirs, Map<String, String> origEnv,
Map<String, String> envMap, List<String> ports, boolean keep,
boolean stdinSupport) {
launch(id, listener, connectionUri, image, command, commandDir,
workingDir, additionalDirs, origEnv, envMap, ports, keep,
stdinSupport, false);
}
/**
* Perform a launch of a command in a container.
*
* @param id
* - id of caller to use to distinguish console owner
* @param listener
* - optional listener of the run console
* @param connectionUri
* - the specified connection to use
* @param image
* - the image to use
* @param command
* - command to run
* @param commandDir
* - directory command requires or null
* @param workingDir
* - working directory or null
* @param additionalDirs
* - additional directories to mount or null
* @param origEnv
* - original environment if we are appending to our existing
* environment
* @param envMap
* - map of environment variable settings
* @param ports
* - ports to expose
* @param keep
* - keep container after running
* @param stdinSupport
* - true if stdin support is required, false otherwise
* @param privilegedMode
* - true if privileged mode is required, false otherwise
* @since 2.1
*/
public void launch(String id, IContainerLaunchListener listener,
final String connectionUri, String image, String command,
String commandDir, String workingDir, List<String> additionalDirs,
Map<String, String> origEnv, Map<String, String> envMap,
List<String> ports, boolean keep, boolean stdinSupport,
boolean privilegedMode) {
launch(id, listener, connectionUri, image, command, commandDir,
workingDir, additionalDirs, origEnv, envMap, ports, keep,
stdinSupport, privilegedMode, null);
}
/**
* Perform a launch of a command in a container.
*
* @param id
* - id of caller to use to distinguish console owner
* @param listener
* - optional listener of the run console
* @param connectionUri
* - the specified connection to use
* @param image
* - the image to use
* @param command
* - command to run
* @param commandDir
* - directory command requires or null
* @param workingDir
* - working directory or null
* @param additionalDirs
* - additional directories to mount or null
* @param origEnv
* - original environment if we are appending to our existing
* environment
* @param envMap
* - map of environment variable settings
* @param ports
* - ports to expose
* @param keep
* - keep container after running
* @param stdinSupport
* - true if stdin support is required, false otherwise
* @param privilegedMode
* - true if privileged mode is required, false otherwise
* @param labels
* - Map of labels for the container
* @since 2.2
*/
public void launch(String id, IContainerLaunchListener listener,
final String connectionUri, String image, String command,
String commandDir, String workingDir, List<String> additionalDirs,
Map<String, String> origEnv, Map<String, String> envMap,
List<String> ports, boolean keep, boolean stdinSupport,
boolean privilegedMode, Map<String, String> labels) {
final String LAUNCH_TITLE = "ContainerLaunch.title"; //$NON-NLS-1$
final String LAUNCH_EXITED_TITLE = "ContainerLaunchExited.title"; //$NON-NLS-1$
final List<String> env = new ArrayList<>();
env.addAll(toList(origEnv));
env.addAll(toList(envMap));
final List<String> cmdList = getCmdList(command);
final Set<String> exposedPorts = new HashSet<>();
final Map<String, List<IDockerPortBinding>> portBindingsMap = new HashMap<>();
if (ports != null) {
for (String port : ports) {
port = port.trim();
if (port.length() > 0) {
String[] segments = port.split(":"); //$NON-NLS-1$
if (segments.length == 1) { // containerPort
exposedPorts.add(segments[0]);
portBindingsMap
.put(segments[0],
Arrays.asList((IDockerPortBinding) new DockerPortBinding(
"", ""))); //$NON-NLS-1$ //$NON-NLS-2$
} else if (segments.length == 2) { // hostPort:containerPort
exposedPorts.add(segments[1]);
portBindingsMap
.put(segments[1],
Arrays.asList((IDockerPortBinding) new DockerPortBinding(
"", segments[0]))); //$NON-NLS-1$ //$NON-NLS-2$
} else if (segments.length == 3) { // either
// ip:hostPort:containerPort
// or ip::containerPort
exposedPorts.add(segments[1]);
if (segments[1].isEmpty()) {
portBindingsMap
.put(segments[2],
Arrays.asList((IDockerPortBinding) new DockerPortBinding(
"", segments[0]))); //$NON-NLS-1$ //$NON-NLS-2$
} else {
portBindingsMap
.put(segments[2],
Arrays.asList((IDockerPortBinding) new DockerPortBinding(
segments[0], segments[1]))); //$NON-NLS-1$ //$NON-NLS-2$
}
}
}
}
}
// Note we only pass volumes to the config if we have a
// remote daemon. Local mounted volumes are passed
// via the HostConfig binds setting
DockerContainerConfig.Builder builder = new DockerContainerConfig.Builder()
.openStdin(stdinSupport)
.cmd(cmdList)
.image(image)
.workingDir(workingDir);
// add any exposed ports as needed
if (exposedPorts.size() > 0)
builder = builder.exposedPorts(exposedPorts);
// add any labels if specified
if (labels != null)
builder = builder.labels(labels);
if (!DockerConnectionManager.getInstance().hasConnections()) {
Display.getDefault()
.syncExec(() -> MessageDialog.openError(
PlatformUI.getWorkbench().getActiveWorkbenchWindow()
.getShell(),
DVMessages.getString(ERROR_LAUNCHING_CONTAINER),
DVMessages.getString(ERROR_NO_CONNECTIONS)));
return;
}
// Try and use the specified connection that was used before,
// otherwise, open an error
final IDockerConnection connection = DockerConnectionManager
.getInstance().getConnectionByUri(connectionUri);
if (connection == null) {
Display.getDefault()
.syncExec(() -> MessageDialog.openError(
PlatformUI.getWorkbench().getActiveWorkbenchWindow()
.getShell(),
DVMessages.getString(ERROR_LAUNCHING_CONTAINER),
DVMessages.getFormattedString(
ERROR_NO_CONNECTION_WITH_URI,
connectionUri)));
return;
}
DockerHostConfig.Builder hostBuilder = new DockerHostConfig.Builder()
.privileged(privilegedMode).securityOpt("seccomp:unconfined"); //$NON-NLS-1$
final Set<String> remoteVolumes = new TreeSet<>();
if (!((DockerConnection) connection).isLocal()) {
// if using remote daemon, we have to
// handle volume mounting differently.
// Instead we mount empty volumes and copy
// the host data over before starting.
if (additionalDirs != null) {
for (String dir : additionalDirs) {
remoteVolumes.add(dir); // $NON-NLS-1$
}
}
if (workingDir != null)
remoteVolumes.add(workingDir); // $NON-NLS-1$
if (commandDir != null)
remoteVolumes.add(commandDir); // $NON-NLS-1$
builder = builder.volumes(remoteVolumes);
} else {
// Running daemon on local host.
// Add mounts for any directories we need to run the executable.
// When we add mount points, we need entries of the form:
// hostname:mountname:Z.
// In our case, we want all directories mounted as-is so the
// executable will run as the user expects.
final List<String> volumes = new ArrayList<>();
if (additionalDirs != null) {
for (String dir : additionalDirs) {
volumes.add(dir + ":" + dir + ":Z"); //$NON-NLS-1$ //$NON-NLS-2$
}
}
if (workingDir != null) {
volumes.add(workingDir + ":" + workingDir + ":Z"); //$NON-NLS-1$ //$NON-NLS-2$
}
if (commandDir != null) {
volumes.add(commandDir + ":" + commandDir + ":Z"); //$NON-NLS-1$ //$NON-NLS-2$
}
hostBuilder = hostBuilder.binds(volumes);
}
final DockerContainerConfig config = builder.build();
// add any port bindings if specified
if (portBindingsMap.size() > 0)
hostBuilder = hostBuilder.portBindings(portBindingsMap);
final IDockerHostConfig hostConfig = hostBuilder.build();
final String imageName = image;
final boolean keepContainer = keep;
final String consoleId = id;
final IContainerLaunchListener containerListener = listener;
Thread t = new Thread(() -> {
// create the container
String containerId = null;
try {
containerId = ((DockerConnection) connection)
.createContainer(config, hostConfig, null);
if (!((DockerConnection) connection).isLocal()) {
// if daemon is remote, we need to copy
// data over from the host.
if (!remoteVolumes.isEmpty()) {
CopyVolumesJob job = new CopyVolumesJob(remoteVolumes,
connection, containerId);
job.schedule();
job.join();
if (job.getResult() != Status.OK_STATUS)
return;
}
}
OutputStream stream = null;
RunConsole oldConsole = getConsole();
final RunConsole rc = RunConsole.findConsole(containerId,
consoleId);
setConsole(rc);
rc.clearConsole();
if (oldConsole != null)
RunConsole.removeConsole(oldConsole);
Display.getDefault()
.syncExec(() -> rc.setTitle(Messages.getFormattedString(
LAUNCH_TITLE, new String[] { cmdList.get(0),
imageName })));
// if (!rc.isAttached()) {
rc.attachToConsole(connection, containerId);
// }
if (rc != null) {
stream = rc.getOutputStream();
if (containerListener != null) {
((ConsoleOutputStream) stream)
.addConsoleListener(containerListener);
}
}
// Create a unique logging thread id which has container id
// and console id
String loggingId = containerId + "." + consoleId;
((DockerConnection) connection).startContainer(containerId,
loggingId, stream);
if (rc != null)
rc.showConsole();
if (containerListener != null) {
IDockerContainerInfo info = ((DockerConnection) connection)
.getContainerInfo(containerId);
containerListener.containerInfo(info);
}
// Wait for the container to finish
final IDockerContainerExit status = ((DockerConnection) connection)
.waitForContainer(containerId);
Display.getDefault().syncExec(() -> {
rc.setTitle(
Messages.getFormattedString(LAUNCH_EXITED_TITLE,
new String[] {
status.statusCode().toString(),
cmdList.get(0), imageName }));
rc.showConsole();
});
// Let any container listener know that the container is
// finished
if (containerListener != null)
containerListener.done();
if (!keepContainer) {
// Drain the logging thread before we remove the
// container (we need to use the logging id)
((DockerConnection) connection)
.stopLoggingThread(loggingId);
while (((DockerConnection) connection).loggingStatus(
loggingId) == EnumDockerLoggingStatus.LOGGING_ACTIVE) {
Thread.sleep(1000);
}
// Look for any Display Log console that the user may
// have opened which would be
// separate and make sure it is removed as well
RunConsole rc2 = RunConsole
.findConsole(((DockerConnection) connection)
.getContainer(containerId));
if (rc2 != null)
RunConsole.removeConsole(rc2);
((DockerConnection) connection)
.removeContainer(containerId);
}
} catch (final DockerException e2) {
// error in creation, try and remove Container if possible
if (!keepContainer && containerId != null) {
try {
((DockerConnection) connection)
.removeContainer(containerId);
} catch (DockerException | InterruptedException e1) {
// ignore exception
}
}
Display.getDefault().syncExec(() -> MessageDialog.openError(
PlatformUI.getWorkbench().getActiveWorkbenchWindow()
.getShell(),
DVMessages.getFormattedString(ERROR_CREATING_CONTAINER,
imageName),
e2.getMessage()));
} catch (InterruptedException e3) {
// for now
// do nothing
}
((DockerConnection) connection).getContainers(true);
});
t.start();
}
private class ID {
private Integer uid;
private Integer gid;
public ID(Integer uid, Integer gid) {
this.uid = uid;
this.gid = gid;
}
public Integer getuid() {
return uid;
}
public Integer getgid() {
return gid;
}
}
/**
* @since 3.0
*/
public int fetchContainerDirs(String connectionUri, String imageName,
List<String> containerDirs, IPath hostDir) {
// Try and use the specified connection that was used before,
// otherwise, open an error
final IDockerConnection connection = DockerConnectionManager
.getInstance().getConnectionByUri(connectionUri);
if (connection == null) {
Display.getDefault()
.syncExec(() -> MessageDialog.openError(
PlatformUI.getWorkbench().getActiveWorkbenchWindow()
.getShell(),
DVMessages.getString(ERROR_LAUNCHING_CONTAINER),
DVMessages.getFormattedString(
ERROR_NO_CONNECTION_WITH_URI,
connectionUri)));
return -1;
}
CopyVolumesFromImageJob job = new CopyVolumesFromImageJob(connection,
imageName, containerDirs, hostDir);
job.schedule();
return 0;
}
/**
* @since 3.0
*/
public Process runCommand(String connectionName, String imageName, IProject project,
IErrorMessageHolder errMsgHolder, String command,
String commandDir,
String workingDir,
List<String> additionalDirs, Map<String, String> origEnv,
Properties envMap, boolean supportStdin,
boolean privilegedMode, HashMap<String, String> labels,
boolean keepContainer) {
Integer uid = null;
Integer gid = null;
// For Unix, make sure that the user id is passed with the run
// so any output files are accessible by this end-user
String os = System.getProperty("os.name"); //$NON-NLS-1$
if (os.indexOf("nux") > 0) { //$NON-NLS-1$
// first try and see if we have already run a command on this
// project
ID ugid = fidMap.get(project);
if (ugid == null) {
try {
uid = (Integer) Files.getAttribute(
project.getLocation().toFile().toPath(),
"unix:uid"); //$NON-NLS-1$
gid = (Integer) Files.getAttribute(
project.getLocation().toFile().toPath(),
"unix:gid"); //$NON-NLS-1$
ugid = new ID(uid, gid);
// store the uid for possible later usage
fidMap.put(project, ugid);
} catch (IOException e) {
// do nothing...leave as null
} // $NON-NLS-1$
} else {
uid = ugid.getuid();
gid = ugid.getgid();
}
}
final List<String> env = new ArrayList<>();
env.addAll(toList(origEnv));
env.addAll(toList(envMap));
final List<String> cmdList = getCmdList(command);
final Map<String, List<IDockerPortBinding>> portBindingsMap = new HashMap<>();
IDockerConnection[] connections = DockerConnectionManager
.getInstance().getConnections();
if (connections == null || connections.length == 0) {
errMsgHolder.setErrorMessage(
Messages.getString("ContainerLaunch.noConnections.error")); //$NON-NLS-1$
return null;
}
IDockerConnection connection = null;
for (IDockerConnection c : connections) {
if (c.getUri().equals(connectionName)) {
connection = c;
break;
}
}
if (connection == null) {
errMsgHolder.setErrorMessage(Messages.getFormattedString(
"ContainerLaunch.connectionNotFound.error", //$NON-NLS-1$
connectionName));
return null;
}
List<IDockerImage> images = connection.getImages();
if (images.isEmpty()) {
errMsgHolder.setErrorMessage(
Messages.getString("ContainerLaunch.noImages.error")); //$NON-NLS-1$
return null;
}
IDockerImageInfo info = connection.getImageInfo(imageName);
if (info == null) {
errMsgHolder.setErrorMessage(Messages.getFormattedString(
"ContainerLaunch.imageNotFound.error", imageName)); //$NON-NLS-1$
return null;
}
DockerContainerConfig.Builder builder = new DockerContainerConfig.Builder()
.openStdin(supportStdin).cmd(cmdList).image(imageName)
.workingDir(workingDir);
// switch to user id for Linux so output is accessible
if (uid != null) {
builder = builder.user(uid.toString());
}
// TODO: add group id here when supported by DockerHostConfig.Builder
// add any labels if specified
if (labels != null)
builder = builder.labels(labels);
DockerHostConfig.Builder hostBuilder = new DockerHostConfig.Builder()
.privileged(privilegedMode);
// Note we only pass volumes to the config if we have a
// remote daemon. Local mounted volumes are passed
// via the HostConfig binds setting
final Set<String> remoteVolumes = new TreeSet<>();
if (!((DockerConnection) connection).isLocal()) {
// if using remote daemon, we have to
// handle volume mounting differently.
// Instead we mount empty volumes and copy
// the host data over before starting.
if (additionalDirs != null) {
for (String dir : additionalDirs) {
IPath p = new Path(dir).removeTrailingSeparator();
remoteVolumes.add(p.toPortableString());
}
}
if (workingDir != null) {
IPath p = new Path(workingDir).removeTrailingSeparator();
remoteVolumes.add(p.toPortableString());
}
if (commandDir != null) {
IPath p = new Path(commandDir).removeTrailingSeparator();
remoteVolumes.add(p.toPortableString());
}
builder = builder.volumes(remoteVolumes);
} else {
// Running daemon on local host.
// Add mounts for any directories we need to run the executable.
// When we add mount points, we need entries of the form:
// hostname:mountname:Z.
// In our case, we want all directories mounted as-is so the
// executable will run as the user expects.
final Set<String> volumes = new TreeSet<>();
if (additionalDirs != null) {
for (String dir : additionalDirs) {
IPath p = new Path(dir).removeTrailingSeparator();
volumes.add(p.toPortableString() + ":" //$NON-NLS-1$
+ p.toPortableString() + ":Z"); //$NON-NLS-1$
}
}
if (workingDir != null) {
IPath p = new Path(workingDir).removeTrailingSeparator();
volumes.add(p.toPortableString() + ":" + p.toPortableString() //$NON-NLS-1$
+ ":Z"); //$NON-NLS-1$
}
if (commandDir != null) {
IPath p = new Path(commandDir).removeTrailingSeparator();
volumes.add(p.toPortableString() + ":" + p.toPortableString() //$NON-NLS-1$
+ ":Z"); //$NON-NLS-1$
}
List<String> volumeList = new ArrayList<>(volumes);
hostBuilder = hostBuilder.binds(volumeList);
}
final DockerContainerConfig config = builder.build();
// add any port bindings if specified
if (portBindingsMap.size() > 0)
hostBuilder = hostBuilder.portBindings(portBindingsMap);
final IDockerHostConfig hostConfig = hostBuilder.build();
// create the container
String containerId = null;
try {
containerId = ((DockerConnection) connection)
.createContainer(config, hostConfig, null);
} catch (DockerException | InterruptedException e) {
errMsgHolder.setErrorMessage(e.getMessage());
return null;
}
final String id = containerId;
final IDockerConnection conn = connection;
Thread t = new Thread(() -> {
if (!((DockerConnection) conn).isLocal()) {
// if daemon is remote, we need to copy
// data over from the host.
if (!remoteVolumes.isEmpty()) {
CopyVolumesJob job = new CopyVolumesJob(remoteVolumes,
conn, id);
job.schedule();
try {
job.join();
} catch (InterruptedException e) {
// ignore
}
if (job.getResult() != Status.OK_STATUS)
return;
}
}
try {
((DockerConnection) conn).startContainer(id,
null);
} catch (DockerException | InterruptedException e) {
// ignore
}
});
t.start();
return new ContainerCommandProcess(connection, imageName, containerId,
keepContainer);
}
/**
* Clean up the container used for launching
*
* @param connectionUri
* the URI of the connection used
* @param info
* the container info
*/
public void cleanup(String connectionUri, IDockerContainerInfo info) {
if (!DockerConnectionManager.getInstance().hasConnections()) {
return;
}
// Try and find the specified connection
final IDockerConnection connection = DockerConnectionManager
.getInstance().getConnectionByUri(connectionUri);
if (connection == null) {
return;
}
try {
connection.killContainer(info.id());
} catch (DockerException | InterruptedException e) {
// do nothing
}
}
/**
* Get the reusable run console for running C/C++ executables in containers.
*
* @return
*/
private RunConsole getConsole() {
// if (console == null) {
// console = RunConsole.getContainerLessConsole();
// }
return console;
}
private void setConsole(RunConsole cons) {
console = cons;
}
/**
* Take the command string and parse it into a list of strings.
*
* @param s
* @return list of strings
*/
private List<String> getCmdList(String s) {
ArrayList<String> list = new ArrayList<>();
int length = s.length();
boolean insideQuote1 = false; // single-quote
boolean insideQuote2 = false; // double-quote
boolean escaped = false;
StringBuffer buffer = new StringBuffer();
// Parse the string and break it up into chunks that are
// separated by white-space or are quoted. Ignore characters
// that have been escaped, including the escape character.
for (int i = 0; i < length; ++i) {
char c = s.charAt(i);
if (escaped) {
buffer.append(c);
escaped = false;
}
switch (c) {
case '\'':
if (!insideQuote2)
insideQuote1 = insideQuote1 ^ true;
else
buffer.append(c);
break;
case '\"':
if (!insideQuote1)
insideQuote2 = insideQuote2 ^ true;
else
buffer.append(c);
break;
case '\\':
escaped = true;
break;
case ' ':
case '\t':
case '\r':
case '\n':
if (insideQuote1 || insideQuote2)
buffer.append(c);
else {
String item = buffer.toString();
buffer.setLength(0);
if (item.length() > 0)
list.add(item);
}
break;
default:
buffer.append(c);
break;
}
}
// add last item of string that will be in the buffer
String item = buffer.toString();
if (item.length() > 0)
list.add(item);
return list;
}
/**
* Convert map of environment variables to a {@link List} of KEY=VALUE
* String
*
* @param variables
* the entries to manipulate
* @return the concatenated key/values for each given variable entry
*/
private List<String> toList(
@SuppressWarnings("rawtypes") final Map variables) {
final List<String> result = new ArrayList<>();
if (variables != null) {
@SuppressWarnings({ "unchecked", "rawtypes" })
Set<Map.Entry> entries = variables.entrySet();
for (@SuppressWarnings("rawtypes")
Map.Entry entry : entries) {
final String key = (String) entry.getKey();
final String value = (String) entry.getValue();
final String envEntry = key + "=" + value; //$NON-NLS-1$
result.add(envEntry);
}
}
return result;
}
/**
* @since 3.0
*/
public Set<String> getCopiedVolumes(String connectionName,
String imageName) {
Set<String> copiedSet = new HashSet<>();
if (copiedVolumesMap != null) {
Map<String, Set<String>> connectionMap = copiedVolumesMap
.get(connectionName);
if (connectionMap != null) {
Set<String> imageSet = connectionMap.get(imageName);
if (imageSet != null) {
copiedSet = imageSet;
}
}
}
return copiedSet;
}
}