YARN-3288. Document and fix indentation in the DockerContainerExecutor code

(cherry picked from commit e0ccea33c9)
This commit is contained in:
Ravi Prakash 2015-03-28 08:00:41 -07:00
parent 274db918c3
commit b1b4951452
6 changed files with 276 additions and 187 deletions

View File

@ -33,6 +33,8 @@ Release 2.8.0 - UNRELEASED
YARN-3397. yarn rmadmin should skip -failover. (J.Andreina via kasha) YARN-3397. yarn rmadmin should skip -failover. (J.Andreina via kasha)
YARN-3288. Document and fix indentation in the DockerContainerExecutor code
OPTIMIZATIONS OPTIMIZATIONS
YARN-3339. TestDockerContainerExecutor should pull a single image and not YARN-3339. TestDockerContainerExecutor should pull a single image and not

View File

@ -210,8 +210,22 @@ public abstract class ContainerExecutor implements Configurable {
} }
} }
public void writeLaunchEnv(OutputStream out, Map<String, String> environment, Map<Path, List<String>> resources, List<String> command) throws IOException{ /**
ContainerLaunch.ShellScriptBuilder sb = ContainerLaunch.ShellScriptBuilder.create(); * This method writes out the launch environment of a container. This can be
* overridden by extending ContainerExecutors to provide different behaviors
* @param out the output stream to which the environment is written (usually
* a script file which will be executed by the Launcher)
* @param environment The environment variables and their values
* @param resources The resources which have been localized for this container
* Symlinks will be created to these localized resources
* @param command The command that will be run.
* @throws IOException if any errors happened writing to the OutputStream,
* while creating symlinks
*/
public void writeLaunchEnv(OutputStream out, Map<String, String> environment,
Map<Path, List<String>> resources, List<String> command) throws IOException{
ContainerLaunch.ShellScriptBuilder sb =
ContainerLaunch.ShellScriptBuilder.create();
if (environment != null) { if (environment != null) {
for (Map.Entry<String,String> env : environment.entrySet()) { for (Map.Entry<String,String> env : environment.entrySet()) {
sb.env(env.getKey().toString(), env.getValue().toString()); sb.env(env.getKey().toString(), env.getValue().toString());

View File

@ -18,10 +18,24 @@
package org.apache.hadoop.yarn.server.nodemanager; package org.apache.hadoop.yarn.server.nodemanager;
import com.google.common.annotations.VisibleForTesting; import static org.apache.hadoop.fs.CreateFlag.CREATE;
import com.google.common.base.Joiner; import static org.apache.hadoop.fs.CreateFlag.OVERWRITE;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings; import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.commons.lang.math.RandomUtils; import org.apache.commons.lang.math.RandomUtils;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
@ -45,38 +59,35 @@ import org.apache.hadoop.yarn.server.nodemanager.containermanager.launcher.Conta
import org.apache.hadoop.yarn.server.nodemanager.containermanager.localizer.ContainerLocalizer; import org.apache.hadoop.yarn.server.nodemanager.containermanager.localizer.ContainerLocalizer;
import org.apache.hadoop.yarn.util.ConverterUtils; import org.apache.hadoop.yarn.util.ConverterUtils;
import java.io.ByteArrayOutputStream; import com.google.common.annotations.VisibleForTesting;
import java.io.DataOutputStream; import com.google.common.base.Joiner;
import java.io.File; import com.google.common.base.Preconditions;
import java.io.IOException; import com.google.common.base.Strings;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.regex.Pattern;
import java.net.InetSocketAddress;
import static org.apache.hadoop.fs.CreateFlag.CREATE;
import static org.apache.hadoop.fs.CreateFlag.OVERWRITE;
/** /**
* This executor will launch a docker container and run the task inside the container. * This executor will launch and run tasks inside Docker containers. It
* currently only supports simple authentication mode. It shares a lot of code
* with the DefaultContainerExecutor (and it may make sense to pull out those
* common pieces later).
*/ */
public class DockerContainerExecutor extends ContainerExecutor { public class DockerContainerExecutor extends ContainerExecutor {
private static final Log LOG = LogFactory private static final Log LOG = LogFactory
.getLog(DockerContainerExecutor.class); .getLog(DockerContainerExecutor.class);
public static final String DOCKER_CONTAINER_EXECUTOR_SCRIPT = "docker_container_executor"; //The name of the script file that will launch the Docker containers
public static final String DOCKER_CONTAINER_EXECUTOR_SESSION_SCRIPT = "docker_container_executor_session"; public static final String DOCKER_CONTAINER_EXECUTOR_SCRIPT =
"docker_container_executor";
// This validates that the image is a proper docker image and would not crash docker. //The name of the session script that the DOCKER_CONTAINER_EXECUTOR_SCRIPT
public static final String DOCKER_IMAGE_PATTERN = "^(([\\w\\.-]+)(:\\d+)*\\/)?[\\w\\.:-]+$"; //launches in turn
public static final String DOCKER_CONTAINER_EXECUTOR_SESSION_SCRIPT =
"docker_container_executor_session";
//This validates that the image is a proper docker image and would not crash
//docker. The image name is not allowed to contain spaces. e.g.
//registry.somecompany.com:9999/containername:0.1 or
//containername:0.1 or
//containername
public static final String DOCKER_IMAGE_PATTERN =
"^(([\\w\\.-]+)(:\\d+)*\\/)?[\\w\\.:-]+$";
private final FileContext lfs; private final FileContext lfs;
private final Pattern dockerImagePattern; private final Pattern dockerImagePattern;
@ -96,14 +107,18 @@ public class DockerContainerExecutor extends ContainerExecutor {
@Override @Override
public void init() throws IOException { public void init() throws IOException {
String auth = getConf().get(CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION); String auth =
getConf().get(CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION);
if (auth != null && !auth.equals("simple")) { if (auth != null && !auth.equals("simple")) {
throw new IllegalStateException("DockerContainerExecutor only works with simple authentication mode"); throw new IllegalStateException(
"DockerContainerExecutor only works with simple authentication mode");
} }
String dockerExecutor = getConf().get(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_EXEC_NAME, String dockerExecutor = getConf().get(
YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_EXEC_NAME,
YarnConfiguration.NM_DEFAULT_DOCKER_CONTAINER_EXECUTOR_EXEC_NAME); YarnConfiguration.NM_DEFAULT_DOCKER_CONTAINER_EXECUTOR_EXEC_NAME);
if (!new File(dockerExecutor).exists()) { if (!new File(dockerExecutor).exists()) {
throw new IllegalStateException("Invalid docker exec path: " + dockerExecutor); throw new IllegalStateException(
"Invalid docker exec path: " + dockerExecutor);
} }
} }
@ -112,7 +127,6 @@ public class DockerContainerExecutor extends ContainerExecutor {
InetSocketAddress nmAddr, String user, String appId, String locId, InetSocketAddress nmAddr, String user, String appId, String locId,
LocalDirsHandlerService dirsHandler) LocalDirsHandlerService dirsHandler)
throws IOException, InterruptedException { throws IOException, InterruptedException {
List<String> localDirs = dirsHandler.getLocalDirs(); List<String> localDirs = dirsHandler.getLocalDirs();
List<String> logDirs = dirsHandler.getLogDirs(); List<String> logDirs = dirsHandler.getLogDirs();
@ -128,7 +142,8 @@ public class DockerContainerExecutor extends ContainerExecutor {
// randomly choose the local directory // randomly choose the local directory
Path appStorageDir = getWorkingDir(localDirs, user, appId); Path appStorageDir = getWorkingDir(localDirs, user, appId);
String tokenFn = String.format(ContainerLocalizer.TOKEN_FILE_NAME_FMT, locId); String tokenFn =
String.format(ContainerLocalizer.TOKEN_FILE_NAME_FMT, locId);
Path tokenDst = new Path(appStorageDir, tokenFn); Path tokenDst = new Path(appStorageDir, tokenFn);
copyFile(nmPrivateContainerTokensPath, tokenDst, user); copyFile(nmPrivateContainerTokensPath, tokenDst, user);
LOG.info("Copying from " + nmPrivateContainerTokensPath + " to " + tokenDst); LOG.info("Copying from " + nmPrivateContainerTokensPath + " to " + tokenDst);
@ -140,20 +155,25 @@ public class DockerContainerExecutor extends ContainerExecutor {
@Override @Override
public int launchContainer(Container container, public int launchContainer(Container container, Path
Path nmPrivateContainerScriptPath, Path nmPrivateTokensPath, nmPrivateContainerScriptPath, Path nmPrivateTokensPath, String userName,
String userName, String appId, Path containerWorkDir, String appId, Path containerWorkDir, List<String> localDirs, List<String>
List<String> localDirs, List<String> logDirs) throws IOException { logDirs) throws IOException {
//Variables for the launch environment can be injected from the command-line
//while submitting the application
String containerImageName = container.getLaunchContext().getEnvironment() String containerImageName = container.getLaunchContext().getEnvironment()
.get(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME); .get(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME);
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("containerImageName from launchContext: " + containerImageName); LOG.debug("containerImageName from launchContext: " + containerImageName);
} }
Preconditions.checkArgument(!Strings.isNullOrEmpty(containerImageName), "Container image must not be null"); Preconditions.checkArgument(!Strings.isNullOrEmpty(containerImageName),
"Container image must not be null");
containerImageName = containerImageName.replaceAll("['\"]", ""); containerImageName = containerImageName.replaceAll("['\"]", "");
Preconditions.checkArgument(saneDockerImage(containerImageName), "Image: " + containerImageName + " is not a proper docker image"); Preconditions.checkArgument(saneDockerImage(containerImageName), "Image: "
String dockerExecutor = getConf().get(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_EXEC_NAME, + containerImageName + " is not a proper docker image");
String dockerExecutor = getConf().get(
YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_EXEC_NAME,
YarnConfiguration.NM_DEFAULT_DOCKER_CONTAINER_EXECUTOR_EXEC_NAME); YarnConfiguration.NM_DEFAULT_DOCKER_CONTAINER_EXECUTOR_EXEC_NAME);
FsPermission dirPerm = new FsPermission(APPDIR_PERM); FsPermission dirPerm = new FsPermission(APPDIR_PERM);
@ -161,10 +181,8 @@ public class DockerContainerExecutor extends ContainerExecutor {
// create container dirs on all disks // create container dirs on all disks
String containerIdStr = ConverterUtils.toString(containerId); String containerIdStr = ConverterUtils.toString(containerId);
String appIdStr = String appIdStr = ConverterUtils.toString(
ConverterUtils.toString( containerId.getApplicationAttemptId().getApplicationId());
containerId.getApplicationAttemptId().
getApplicationId());
for (String sLocalDir : localDirs) { for (String sLocalDir : localDirs) {
Path usersdir = new Path(sLocalDir, ContainerLocalizer.USERCACHE); Path usersdir = new Path(sLocalDir, ContainerLocalizer.USERCACHE);
Path userdir = new Path(usersdir, userName); Path userdir = new Path(usersdir, userName);
@ -191,12 +209,17 @@ public class DockerContainerExecutor extends ContainerExecutor {
new Path(containerWorkDir, ContainerLaunch.FINAL_CONTAINER_TOKENS_FILE); new Path(containerWorkDir, ContainerLaunch.FINAL_CONTAINER_TOKENS_FILE);
lfs.util().copy(nmPrivateTokensPath, tokenDst); lfs.util().copy(nmPrivateTokensPath, tokenDst);
String localDirMount = toMount(localDirs); String localDirMount = toMount(localDirs);
String logDirMount = toMount(logDirs); String logDirMount = toMount(logDirs);
String containerWorkDirMount = toMount(Collections.singletonList(containerWorkDir.toUri().getPath())); String containerWorkDirMount = toMount(Collections.singletonList(
containerWorkDir.toUri().getPath()));
StringBuilder commands = new StringBuilder(); StringBuilder commands = new StringBuilder();
//Use docker run to launch the docker container. See man pages for
//docker-run
//--rm removes the container automatically once the container finishes
//--net=host allows the container to take on the host's network stack
//--name sets the Docker Container name to the YARN containerId string
//-v is used to bind mount volumes for local, log and work dirs.
String commandStr = commands.append(dockerExecutor) String commandStr = commands.append(dockerExecutor)
.append(" ") .append(" ")
.append("run") .append("run")
@ -210,14 +233,20 @@ public class DockerContainerExecutor extends ContainerExecutor {
.append(" ") .append(" ")
.append(containerImageName) .append(containerImageName)
.toString(); .toString();
String dockerPidScript = "`" + dockerExecutor + " inspect --format {{.State.Pid}} " + containerIdStr + "`"; //Get the pid of the process which has been launched as a docker container
//using docker inspect
String dockerPidScript = "`" + dockerExecutor +
" inspect --format {{.State.Pid}} " + containerIdStr + "`";
// Create new local launch wrapper script // Create new local launch wrapper script
LocalWrapperScriptBuilder sb = LocalWrapperScriptBuilder sb = new UnixLocalWrapperScriptBuilder(
new UnixLocalWrapperScriptBuilder(containerWorkDir, commandStr, dockerPidScript); containerWorkDir, commandStr, dockerPidScript);
Path pidFile = getPidFilePath(containerId); Path pidFile = getPidFilePath(containerId);
if (pidFile != null) { if (pidFile != null) {
sb.writeLocalWrapperScript(launchDst, pidFile); sb.writeLocalWrapperScript(launchDst, pidFile);
} else { } else {
//Although the container was activated by ContainerLaunch before exec()
//was called, since then deactivateContainer() has been called.
LOG.info("Container " + containerIdStr LOG.info("Container " + containerIdStr
+ " was marked as inactive. Returning terminated error"); + " was marked as inactive. Returning terminated error");
return ExitCode.TERMINATED.getExitCode(); return ExitCode.TERMINATED.getExitCode();
@ -234,7 +263,8 @@ public class DockerContainerExecutor extends ContainerExecutor {
String[] command = getRunCommand(sb.getWrapperScriptPath().toString(), String[] command = getRunCommand(sb.getWrapperScriptPath().toString(),
containerIdStr, userName, pidFile, this.getConf()); containerIdStr, userName, pidFile, this.getConf());
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("launchContainer: " + commandStr + " " + Joiner.on(" ").join(command)); LOG.debug("launchContainer: " + commandStr + " " +
Joiner.on(" ").join(command));
} }
shExec = new ShellCommandExecutor( shExec = new ShellCommandExecutor(
command, command,
@ -279,9 +309,17 @@ public class DockerContainerExecutor extends ContainerExecutor {
} }
@Override @Override
public void writeLaunchEnv(OutputStream out, Map<String, String> environment, Map<Path, List<String>> resources, List<String> command) throws IOException { /**
ContainerLaunch.ShellScriptBuilder sb = ContainerLaunch.ShellScriptBuilder.create(); * Filter the environment variables that may conflict with the ones set in
* the docker image and write them out to an OutputStream.
*/
public void writeLaunchEnv(OutputStream out, Map<String, String> environment,
Map<Path, List<String>> resources, List<String> command)
throws IOException {
ContainerLaunch.ShellScriptBuilder sb =
ContainerLaunch.ShellScriptBuilder.create();
//Remove environments that may conflict with the ones in Docker image.
Set<String> exclusionSet = new HashSet<String>(); Set<String> exclusionSet = new HashSet<String>();
exclusionSet.add(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME); exclusionSet.add(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME);
exclusionSet.add(ApplicationConstants.Environment.HADOOP_YARN_HOME.name()); exclusionSet.add(ApplicationConstants.Environment.HADOOP_YARN_HOME.name());
@ -427,6 +465,9 @@ public class DockerContainerExecutor extends ContainerExecutor {
return builder.toString(); return builder.toString();
} }
//This class facilitates (only) the creation of platform-specific scripts that
//will be used to launch the containers
//TODO: This should be re-used from the DefaultContainerExecutor.
private abstract class LocalWrapperScriptBuilder { private abstract class LocalWrapperScriptBuilder {
private final Path wrapperScriptPath; private final Path wrapperScriptPath;
@ -435,7 +476,8 @@ public class DockerContainerExecutor extends ContainerExecutor {
return wrapperScriptPath; return wrapperScriptPath;
} }
public void writeLocalWrapperScript(Path launchDst, Path pidFile) throws IOException { public void writeLocalWrapperScript(Path launchDst, Path pidFile)
throws IOException {
DataOutputStream out = null; DataOutputStream out = null;
PrintStream pout = null; PrintStream pout = null;
@ -448,8 +490,8 @@ public class DockerContainerExecutor extends ContainerExecutor {
} }
} }
protected abstract void writeLocalWrapperScript(Path launchDst, Path pidFile, protected abstract void writeLocalWrapperScript(Path launchDst,
PrintStream pout); Path pidFile, PrintStream pout);
protected LocalWrapperScriptBuilder(Path containerWorkDir) { protected LocalWrapperScriptBuilder(Path containerWorkDir) {
this.wrapperScriptPath = new Path(containerWorkDir, this.wrapperScriptPath = new Path(containerWorkDir,
@ -457,13 +499,15 @@ public class DockerContainerExecutor extends ContainerExecutor {
} }
} }
//TODO: This class too should be used from DefaultContainerExecutor.
private final class UnixLocalWrapperScriptBuilder private final class UnixLocalWrapperScriptBuilder
extends LocalWrapperScriptBuilder { extends LocalWrapperScriptBuilder {
private final Path sessionScriptPath; private final Path sessionScriptPath;
private final String dockerCommand; private final String dockerCommand;
private final String dockerPidScript; private final String dockerPidScript;
public UnixLocalWrapperScriptBuilder(Path containerWorkDir, String dockerCommand, String dockerPidScript) { public UnixLocalWrapperScriptBuilder(Path containerWorkDir,
String dockerCommand, String dockerPidScript) {
super(containerWorkDir); super(containerWorkDir);
this.dockerCommand = dockerCommand; this.dockerCommand = dockerCommand;
this.dockerPidScript = dockerPidScript; this.dockerPidScript = dockerPidScript;
@ -481,7 +525,6 @@ public class DockerContainerExecutor extends ContainerExecutor {
@Override @Override
public void writeLocalWrapperScript(Path launchDst, Path pidFile, public void writeLocalWrapperScript(Path launchDst, Path pidFile,
PrintStream pout) { PrintStream pout) {
String exitCodeFile = ContainerLaunch.getExitCodeFile( String exitCodeFile = ContainerLaunch.getExitCodeFile(
pidFile.toString()); pidFile.toString());
String tmpFile = exitCodeFile + ".tmp"; String tmpFile = exitCodeFile + ".tmp";
@ -505,7 +548,8 @@ public class DockerContainerExecutor extends ContainerExecutor {
// hence write pid to tmp file first followed by a mv // hence write pid to tmp file first followed by a mv
pout.println("#!/usr/bin/env bash"); pout.println("#!/usr/bin/env bash");
pout.println(); pout.println();
pout.println("echo "+ dockerPidScript +" > " + pidFile.toString() + ".tmp"); pout.println("echo "+ dockerPidScript +" > " + pidFile.toString()
+ ".tmp");
pout.println("/bin/mv -f " + pidFile.toString() + ".tmp " + pidFile); pout.println("/bin/mv -f " + pidFile.toString() + ".tmp " + pidFile);
pout.println(dockerCommand + " bash \"" + pout.println(dockerCommand + " bash \"" +
launchDst.toUri().getPath().toString() + "\""); launchDst.toUri().getPath().toString() + "\"");
@ -538,7 +582,8 @@ public class DockerContainerExecutor extends ContainerExecutor {
for (String localDir : localDirs) { for (String localDir : localDirs) {
// create $local.dir/usercache/$user and its immediate parent // create $local.dir/usercache/$user and its immediate parent
try { try {
createDir(getUserCacheDir(new Path(localDir), user), userperms, true, user); createDir(getUserCacheDir(new Path(localDir), user), userperms, true,
user);
} catch (IOException e) { } catch (IOException e) {
LOG.warn("Unable to create the user directory : " + localDir, e); LOG.warn("Unable to create the user directory : " + localDir, e);
continue; continue;

View File

@ -269,8 +269,8 @@ public class ContainerLaunch implements Callable<Integer> {
localResources, nmPrivateClasspathJarDir); localResources, nmPrivateClasspathJarDir);
// Write out the environment // Write out the environment
exec.writeLaunchEnv(containerScriptOutStream, environment, localResources, exec.writeLaunchEnv(containerScriptOutStream, environment,
launchContext.getCommands()); localResources, launchContext.getCommands());
// /////////// End of writing out container-script // /////////// End of writing out container-script

View File

@ -18,7 +18,18 @@
package org.apache.hadoop.yarn.server.nodemanager; package org.apache.hadoop.yarn.server.nodemanager;
import com.google.common.base.Strings; import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configuration;
@ -26,43 +37,24 @@ import org.apache.hadoop.fs.FileContext;
import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.permission.FsPermission; import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.util.Shell; import org.apache.hadoop.util.Shell;
import org.apache.hadoop.yarn.api.ApplicationConstants;
import org.apache.hadoop.yarn.api.records.ContainerId; import org.apache.hadoop.yarn.api.records.ContainerId;
import org.apache.hadoop.yarn.api.records.ContainerLaunchContext; import org.apache.hadoop.yarn.api.records.ContainerLaunchContext;
import org.apache.hadoop.yarn.conf.YarnConfiguration; import org.apache.hadoop.yarn.conf.YarnConfiguration;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.container.Container; import org.apache.hadoop.yarn.server.nodemanager.containermanager.container.Container;
import org.apache.hadoop.yarn.util.ConverterUtils;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import java.io.File; import com.google.common.base.Strings;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/** /**
* This is intended to test the DockerContainerExecutor code, but it requires docker * This is intended to test the DockerContainerExecutor code, but it requires
* to be installed. * docker to be installed.
* <br><ol> * <br><ol>
* <li>Install docker, and Compile the code with docker-service-url set to the host and port * <li>Install docker, and Compile the code with docker-service-url set to the
* where docker service is running. * host and port where docker service is running.
* <br><pre><code> * <br><pre><code>
* > mvn clean install -Ddocker-service-url=tcp://0.0.0.0:4243 * > mvn clean install -Ddocker-service-url=tcp://0.0.0.0:4243 -DskipTests
* -DskipTests
* </code></pre> * </code></pre>
*/ */
public class TestDockerContainerExecutor { public class TestDockerContainerExecutor {
@ -75,14 +67,10 @@ public class TestDockerContainerExecutor {
private FileContext lfs; private FileContext lfs;
private String yarnImage; private String yarnImage;
private int id = 0;
private String appSubmitter; private String appSubmitter;
private String dockerUrl; private String dockerUrl;
private String testImage = "centos:latest"; private String testImage = "centos:latest";
private String dockerExec; private String dockerExec;
private String containerIdStr;
private ContainerId getNextContainerId() { private ContainerId getNextContainerId() {
ContainerId cId = mock(ContainerId.class, RETURNS_DEEP_STUBS); ContainerId cId = mock(ContainerId.class, RETURNS_DEEP_STUBS);
String id = "CONTAINER_" + System.currentTimeMillis(); String id = "CONTAINER_" + System.currentTimeMillis();
@ -91,6 +79,8 @@ public class TestDockerContainerExecutor {
} }
@Before @Before
//Initialize a new DockerContainerExecutor that will be used to launch mocked
//containers.
public void setup() { public void setup() {
try { try {
lfs = FileContext.getLocalFSFileContext(); lfs = FileContext.getLocalFSFileContext();
@ -113,8 +103,10 @@ public class TestDockerContainerExecutor {
} }
dockerUrl = " -H " + dockerUrl; dockerUrl = " -H " + dockerUrl;
dockerExec = "docker " + dockerUrl; dockerExec = "docker " + dockerUrl;
conf.set(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, yarnImage); conf.set(
conf.set(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_EXEC_NAME, dockerExec); YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, yarnImage);
conf.set(
YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_EXEC_NAME, dockerExec);
exec = new DockerContainerExecutor(); exec = new DockerContainerExecutor();
dirsHandler = new LocalDirsHandlerService(); dirsHandler = new LocalDirsHandlerService();
dirsHandler.init(conf); dirsHandler.init(conf);
@ -129,7 +121,6 @@ public class TestDockerContainerExecutor {
private Shell.ShellCommandExecutor shellExec(String command) { private Shell.ShellCommandExecutor shellExec(String command) {
try { try {
Shell.ShellCommandExecutor shExec = new Shell.ShellCommandExecutor( Shell.ShellCommandExecutor shExec = new Shell.ShellCommandExecutor(
command.split("\\s+"), command.split("\\s+"),
new File(workDir.toUri().getPath()), new File(workDir.toUri().getPath()),
@ -145,14 +136,24 @@ public class TestDockerContainerExecutor {
return exec != null; return exec != null;
} }
private int runAndBlock(ContainerId cId, Map<String, String> launchCtxEnv, String... cmd) throws IOException { /**
* Test that a docker container can be launched to run a command
* @param cId a fake ContainerID
* @param launchCtxEnv
* @param cmd the command to launch inside the docker container
* @return the exit code of the process used to launch the docker container
* @throws IOException
*/
private int runAndBlock(ContainerId cId, Map<String, String> launchCtxEnv,
String... cmd) throws IOException {
String appId = "APP_" + System.currentTimeMillis(); String appId = "APP_" + System.currentTimeMillis();
Container container = mock(Container.class); Container container = mock(Container.class);
ContainerLaunchContext context = mock(ContainerLaunchContext.class); ContainerLaunchContext context = mock(ContainerLaunchContext.class);
when(container.getContainerId()).thenReturn(cId); when(container.getContainerId()).thenReturn(cId);
when(container.getLaunchContext()).thenReturn(context); when(container.getLaunchContext()).thenReturn(context);
when(cId.getApplicationAttemptId().getApplicationId().toString()).thenReturn(appId); when(cId.getApplicationAttemptId().getApplicationId().toString())
.thenReturn(appId);
when(context.getEnvironment()).thenReturn(launchCtxEnv); when(context.getEnvironment()).thenReturn(launchCtxEnv);
String script = writeScriptFile(launchCtxEnv, cmd); String script = writeScriptFile(launchCtxEnv, cmd);
@ -168,7 +169,9 @@ public class TestDockerContainerExecutor {
dirsHandler.getLogDirs()); dirsHandler.getLogDirs());
} }
private String writeScriptFile(Map<String, String> launchCtxEnv, String... cmd) throws IOException { // Write the script used to launch the docker container in a temp file
private String writeScriptFile(Map<String, String> launchCtxEnv,
String... cmd) throws IOException {
File f = File.createTempFile("TestDockerContainerExecutor", ".sh"); File f = File.createTempFile("TestDockerContainerExecutor", ".sh");
f.deleteOnExit(); f.deleteOnExit();
PrintWriter p = new PrintWriter(new FileOutputStream(f)); PrintWriter p = new PrintWriter(new FileOutputStream(f));
@ -193,6 +196,10 @@ public class TestDockerContainerExecutor {
} }
} }
/**
* Test that a touch command can be launched successfully in a docker
* container
*/
@Test @Test
public void testLaunchContainer() throws IOException { public void testLaunchContainer() throws IOException {
if (!shouldRun()) { if (!shouldRun()) {
@ -201,12 +208,13 @@ public class TestDockerContainerExecutor {
} }
Map<String, String> env = new HashMap<String, String>(); Map<String, String> env = new HashMap<String, String>();
env.put(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, testImage); env.put(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME,
testImage);
String touchFileName = "touch-file-" + System.currentTimeMillis(); String touchFileName = "touch-file-" + System.currentTimeMillis();
File touchFile = new File(dirsHandler.getLocalDirs().get(0), touchFileName); File touchFile = new File(dirsHandler.getLocalDirs().get(0), touchFileName);
ContainerId cId = getNextContainerId(); ContainerId cId = getNextContainerId();
int ret = runAndBlock( int ret = runAndBlock(cId, env, "touch", touchFile.getAbsolutePath(), "&&",
cId, env, "touch", touchFile.getAbsolutePath(), "&&", "cp", touchFile.getAbsolutePath(), "/"); "cp", touchFile.getAbsolutePath(), "/");
assertEquals(0, ret); assertEquals(0, ret);
} }

View File

@ -18,7 +18,23 @@
package org.apache.hadoop.yarn.server.nodemanager; package org.apache.hadoop.yarn.server.nodemanager;
import com.google.common.base.Strings; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configuration;
@ -36,23 +52,6 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
/** /**
* Mock tests for docker container executor * Mock tests for docker container executor
*/ */
@ -81,8 +80,10 @@ public class TestDockerContainerExecutorWithMocks {
conf.set(YarnConfiguration.NM_LINUX_CONTAINER_EXECUTOR_PATH, executorPath); conf.set(YarnConfiguration.NM_LINUX_CONTAINER_EXECUTOR_PATH, executorPath);
conf.set(YarnConfiguration.NM_LOCAL_DIRS, "/tmp/nm-local-dir" + time); conf.set(YarnConfiguration.NM_LOCAL_DIRS, "/tmp/nm-local-dir" + time);
conf.set(YarnConfiguration.NM_LOG_DIRS, "/tmp/userlogs" + time); conf.set(YarnConfiguration.NM_LOG_DIRS, "/tmp/userlogs" + time);
conf.set(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, yarnImage); conf.set(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME,
conf.set(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_EXEC_NAME , DOCKER_LAUNCH_COMMAND); yarnImage);
conf.set(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_EXEC_NAME,
DOCKER_LAUNCH_COMMAND);
dockerContainerExecutor = new DockerContainerExecutor(); dockerContainerExecutor = new DockerContainerExecutor();
dirsHandler = new LocalDirsHandlerService(); dirsHandler = new LocalDirsHandlerService();
dirsHandler.init(conf); dirsHandler.init(conf);
@ -95,7 +96,6 @@ public class TestDockerContainerExecutorWithMocks {
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@After @After
@ -110,6 +110,8 @@ public class TestDockerContainerExecutorWithMocks {
} }
@Test(expected = IllegalStateException.class) @Test(expected = IllegalStateException.class)
//Test that DockerContainerExecutor doesn't successfully init on a secure
//cluster
public void testContainerInitSecure() throws IOException { public void testContainerInitSecure() throws IOException {
dockerContainerExecutor.getConf().set( dockerContainerExecutor.getConf().set(
CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION, "kerberos"); CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION, "kerberos");
@ -117,6 +119,8 @@ public class TestDockerContainerExecutorWithMocks {
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
//Test that when the image name is null, the container launch throws an
//IllegalArgumentException
public void testContainerLaunchNullImage() throws IOException { public void testContainerLaunchNullImage() throws IOException {
String appSubmitter = "nobody"; String appSubmitter = "nobody";
String appId = "APP_ID"; String appId = "APP_ID";
@ -126,17 +130,19 @@ public class TestDockerContainerExecutorWithMocks {
Container container = mock(Container.class, RETURNS_DEEP_STUBS); Container container = mock(Container.class, RETURNS_DEEP_STUBS);
ContainerId cId = mock(ContainerId.class, RETURNS_DEEP_STUBS); ContainerId cId = mock(ContainerId.class, RETURNS_DEEP_STUBS);
ContainerLaunchContext context = mock(ContainerLaunchContext.class); ContainerLaunchContext context = mock(ContainerLaunchContext.class);
HashMap<String, String> env = new HashMap<String,String>();
HashMap<String, String> env = new HashMap<String,String>();
when(container.getContainerId()).thenReturn(cId); when(container.getContainerId()).thenReturn(cId);
when(container.getLaunchContext()).thenReturn(context); when(container.getLaunchContext()).thenReturn(context);
when(cId.getApplicationAttemptId().getApplicationId().toString()).thenReturn(appId); when(cId.getApplicationAttemptId().getApplicationId().toString())
.thenReturn(appId);
when(cId.toString()).thenReturn(containerId); when(cId.toString()).thenReturn(containerId);
when(context.getEnvironment()).thenReturn(env); when(context.getEnvironment()).thenReturn(env);
env.put(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, testImage); env.put(
dockerContainerExecutor.getConf() YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, testImage);
.set(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, testImage); dockerContainerExecutor.getConf().set(
YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, testImage);
Path scriptPath = new Path("file:///bin/echo"); Path scriptPath = new Path("file:///bin/echo");
Path tokensPath = new Path("file:///dev/null"); Path tokensPath = new Path("file:///dev/null");
@ -149,6 +155,8 @@ public class TestDockerContainerExecutorWithMocks {
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
//Test that when the image name is invalid, the container launch throws an
//IllegalArgumentException
public void testContainerLaunchInvalidImage() throws IOException { public void testContainerLaunchInvalidImage() throws IOException {
String appSubmitter = "nobody"; String appSubmitter = "nobody";
String appId = "APP_ID"; String appId = "APP_ID";
@ -162,13 +170,15 @@ public class TestDockerContainerExecutorWithMocks {
when(container.getContainerId()).thenReturn(cId); when(container.getContainerId()).thenReturn(cId);
when(container.getLaunchContext()).thenReturn(context); when(container.getLaunchContext()).thenReturn(context);
when(cId.getApplicationAttemptId().getApplicationId().toString()).thenReturn(appId); when(cId.getApplicationAttemptId().getApplicationId().toString())
.thenReturn(appId);
when(cId.toString()).thenReturn(containerId); when(cId.toString()).thenReturn(containerId);
when(context.getEnvironment()).thenReturn(env); when(context.getEnvironment()).thenReturn(env);
env.put(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, testImage); env.put(
dockerContainerExecutor.getConf() YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, testImage);
.set(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, testImage); dockerContainerExecutor.getConf().set(
YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, testImage);
Path scriptPath = new Path("file:///bin/echo"); Path scriptPath = new Path("file:///bin/echo");
Path tokensPath = new Path("file:///dev/null"); Path tokensPath = new Path("file:///dev/null");
@ -181,6 +191,8 @@ public class TestDockerContainerExecutorWithMocks {
} }
@Test @Test
//Test that a container launch correctly wrote the session script with the
//commands we expected
public void testContainerLaunch() throws IOException { public void testContainerLaunch() throws IOException {
String appSubmitter = "nobody"; String appSubmitter = "nobody";
String appId = "APP_ID"; String appId = "APP_ID";
@ -194,40 +206,48 @@ public class TestDockerContainerExecutorWithMocks {
when(container.getContainerId()).thenReturn(cId); when(container.getContainerId()).thenReturn(cId);
when(container.getLaunchContext()).thenReturn(context); when(container.getLaunchContext()).thenReturn(context);
when(cId.getApplicationAttemptId().getApplicationId().toString()).thenReturn(appId); when(cId.getApplicationAttemptId().getApplicationId().toString())
.thenReturn(appId);
when(cId.toString()).thenReturn(containerId); when(cId.toString()).thenReturn(containerId);
when(context.getEnvironment()).thenReturn(env); when(context.getEnvironment()).thenReturn(env);
env.put(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, testImage); env.put(
YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, testImage);
Path scriptPath = new Path("file:///bin/echo"); Path scriptPath = new Path("file:///bin/echo");
Path tokensPath = new Path("file:///dev/null"); Path tokensPath = new Path("file:///dev/null");
Path pidFile = new Path(workDir, "pid"); Path pidFile = new Path(workDir, "pid");
dockerContainerExecutor.activateContainer(cId, pidFile); dockerContainerExecutor.activateContainer(cId, pidFile);
int ret = dockerContainerExecutor.launchContainer(container, scriptPath, tokensPath, int ret = dockerContainerExecutor.launchContainer(container, scriptPath,
appSubmitter, appId, workDir, dirsHandler.getLocalDirs(), tokensPath, appSubmitter, appId, workDir, dirsHandler.getLocalDirs(),
dirsHandler.getLogDirs()); dirsHandler.getLogDirs());
assertEquals(0, ret); assertEquals(0, ret);
//get the script //get the script
Path sessionScriptPath = new Path(workDir, Path sessionScriptPath = new Path(workDir,
Shell.appendScriptExtension( Shell.appendScriptExtension(
DockerContainerExecutor.DOCKER_CONTAINER_EXECUTOR_SESSION_SCRIPT)); DockerContainerExecutor.DOCKER_CONTAINER_EXECUTOR_SESSION_SCRIPT));
LineNumberReader lnr = new LineNumberReader(new FileReader(sessionScriptPath.toString())); LineNumberReader lnr = new LineNumberReader(new FileReader(
sessionScriptPath.toString()));
boolean cmdFound = false; boolean cmdFound = false;
List<String> localDirs = dirsToMount(dirsHandler.getLocalDirs()); List<String> localDirs = dirsToMount(dirsHandler.getLocalDirs());
List<String> logDirs = dirsToMount(dirsHandler.getLogDirs()); List<String> logDirs = dirsToMount(dirsHandler.getLogDirs());
List<String> workDirMount = dirsToMount(Collections.singletonList(workDir.toUri().getPath())); List<String> workDirMount = dirsToMount(Collections.singletonList(
List<String> expectedCommands = new ArrayList<String>( workDir.toUri().getPath()));
Arrays.asList(DOCKER_LAUNCH_COMMAND, "run", "--rm", "--net=host", "--name", containerId)); List<String> expectedCommands = new ArrayList<String>(Arrays.asList(
DOCKER_LAUNCH_COMMAND, "run", "--rm", "--net=host", "--name",
containerId));
expectedCommands.addAll(localDirs); expectedCommands.addAll(localDirs);
expectedCommands.addAll(logDirs); expectedCommands.addAll(logDirs);
expectedCommands.addAll(workDirMount); expectedCommands.addAll(workDirMount);
String shellScript = workDir + "/launch_container.sh"; String shellScript = workDir + "/launch_container.sh";
expectedCommands.addAll(Arrays.asList(testImage.replaceAll("['\"]", ""), "bash","\"" + shellScript + "\"")); expectedCommands.addAll(Arrays.asList(testImage.replaceAll("['\"]", ""),
"bash","\"" + shellScript + "\""));
String expectedPidString = "echo `/bin/true inspect --format {{.State.Pid}} " + containerId+"` > "+ pidFile.toString() + ".tmp"; String expectedPidString =
"echo `/bin/true inspect --format {{.State.Pid}} " + containerId+"` > "+
pidFile.toString() + ".tmp";
boolean pidSetterFound = false; boolean pidSetterFound = false;
while(lnr.ready()){ while(lnr.ready()){
String line = lnr.readLine(); String line = lnr.readLine();