blob: 5e3e15ca78c394f5f3ea566cc9d354f368abfd59 [file] [log] [blame]
/*
* *
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* /
*/
package org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.runtime;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.registry.client.binding.RegistryPathUtils;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.authorize.AccessControlList;
import org.apache.hadoop.util.Shell;
import org.apache.hadoop.util.StringUtils;
import org.apache.hadoop.yarn.conf.YarnConfiguration;
import org.apache.hadoop.yarn.server.nodemanager.ContainerExecutor;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.container.Container;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.launcher.ContainerLaunch;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.privileged.PrivilegedOperation;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.privileged.PrivilegedOperationException;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.privileged.PrivilegedOperationExecutor;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.resources.CGroupsHandler;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.resources.ResourceHandlerModule;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.runtime.docker.DockerClient;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.runtime.docker.DockerInspectCommand;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.runtime.docker.DockerRunCommand;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.runtime.docker.DockerStopCommand;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.runtime.ContainerExecutionException;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.runtime.ContainerRuntime;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.runtime.ContainerRuntimeConstants;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.runtime.ContainerRuntimeContext;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;
import static org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.runtime.LinuxContainerRuntimeConstants.*;
/**
* <p>This class is a {@link ContainerRuntime} implementation that uses the
* native {@code container-executor} binary via a
* {@link PrivilegedOperationExecutor} instance to launch processes inside
* Docker containers.</p>
*
* <p>The following environment variables are used to configure the Docker
* engine:</p>
*
* <ul>
* <li>
* {@code YARN_CONTAINER_RUNTIME_TYPE} ultimately determines whether a
* Docker container will be used. If the value is {@code docker}, a Docker
* container will be used. Otherwise a regular process tree container will
* be used. This environment variable is checked by the
* {@link #isDockerContainerRequested} method, which is called by the
* {@link DelegatingLinuxContainerRuntime}.
* </li>
* <li>
* {@code YARN_CONTAINER_RUNTIME_DOCKER_IMAGE} names which image
* will be used to launch the Docker container.
* </li>
* <li>
* {@code YARN_CONTAINER_RUNTIME_DOCKER_IMAGE_FILE} is currently ignored.
* </li>
* <li>
* {@code YARN_CONTAINER_RUNTIME_DOCKER_RUN_OVERRIDE_DISABLE} controls
* whether the Docker container's default command is overridden. When set
* to {@code true}, the Docker container's command will be
* {@code bash <path_to_launch_script>}. When unset or set to {@code false}
* the Docker container's default command is used.
* </li>
* <li>
* {@code YARN_CONTAINER_RUNTIME_DOCKER_CONTAINER_NETWORK} sets the
* network type to be used by the Docker container. It must be a valid
* value as determined by the
* {@code yarn.nodemanager.runtime.linux.docker.allowed-container-networks}
* property.
* </li>
* <li>
* {@code YARN_CONTAINER_RUNTIME_DOCKER_CONTAINER_HOSTNAME} sets the
* hostname to be used by the Docker container. If not specified, a
* hostname will be derived from the container ID.
* </li>
* <li>
* {@code YARN_CONTAINER_RUNTIME_DOCKER_RUN_PRIVILEGED_CONTAINER}
* controls whether the Docker container is a privileged container. In order
* to use privileged containers, the
* {@code yarn.nodemanager.runtime.linux.docker.privileged-containers.allowed}
* property must be set to {@code true}, and the application owner must
* appear in the value of the
* {@code yarn.nodemanager.runtime.linux.docker.privileged-containers.acl}
* property. If this environment variable is set to {@code true}, a
* privileged Docker container will be used if allowed. No other value is
* allowed, so the environment variable should be left unset rather than
* setting it to false.
* </li>
* <li>
* {@code YARN_CONTAINER_RUNTIME_DOCKER_LOCAL_RESOURCE_MOUNTS} adds
* additional volume mounts to the Docker container. The value of the
* environment variable should be a comma-separated list of mounts.
* All such mounts must be given as {@code source:dest}, where the
* source is an absolute path that is not a symlink and that points to a
* localized resource.
* </li>
* </ul>
*/
@InterfaceAudience.Private
@InterfaceStability.Unstable
public class DockerLinuxContainerRuntime implements LinuxContainerRuntime {
private static final Logger LOG =
LoggerFactory.getLogger(DockerLinuxContainerRuntime.class);
// This validates that the image is a proper docker image
public static final String DOCKER_IMAGE_PATTERN =
"^(([a-zA-Z0-9.-]+)(:\\d+)?/)?([a-z0-9_./-]+)(:[\\w.-]+)?$";
private static final Pattern dockerImagePattern =
Pattern.compile(DOCKER_IMAGE_PATTERN);
public static final String HOSTNAME_PATTERN =
"^[a-zA-Z0-9][a-zA-Z0-9_.-]+$";
private static final Pattern hostnamePattern = Pattern.compile(
HOSTNAME_PATTERN);
@InterfaceAudience.Private
public static final String ENV_DOCKER_CONTAINER_IMAGE =
"YARN_CONTAINER_RUNTIME_DOCKER_IMAGE";
@InterfaceAudience.Private
public static final String ENV_DOCKER_CONTAINER_IMAGE_FILE =
"YARN_CONTAINER_RUNTIME_DOCKER_IMAGE_FILE";
@InterfaceAudience.Private
public static final String ENV_DOCKER_CONTAINER_RUN_OVERRIDE_DISABLE =
"YARN_CONTAINER_RUNTIME_DOCKER_RUN_OVERRIDE_DISABLE";
@InterfaceAudience.Private
public static final String ENV_DOCKER_CONTAINER_NETWORK =
"YARN_CONTAINER_RUNTIME_DOCKER_CONTAINER_NETWORK";
@InterfaceAudience.Private
public static final String ENV_DOCKER_CONTAINER_HOSTNAME =
"YARN_CONTAINER_RUNTIME_DOCKER_CONTAINER_HOSTNAME";
@InterfaceAudience.Private
public static final String ENV_DOCKER_CONTAINER_RUN_PRIVILEGED_CONTAINER =
"YARN_CONTAINER_RUNTIME_DOCKER_RUN_PRIVILEGED_CONTAINER";
@InterfaceAudience.Private
public static final String ENV_DOCKER_CONTAINER_RUN_ENABLE_USER_REMAPPING =
"YARN_CONTAINER_RUNTIME_DOCKER_RUN_ENABLE_USER_REMAPPING";
@InterfaceAudience.Private
public static final String ENV_DOCKER_CONTAINER_LOCAL_RESOURCE_MOUNTS =
"YARN_CONTAINER_RUNTIME_DOCKER_LOCAL_RESOURCE_MOUNTS";
private Configuration conf;
private DockerClient dockerClient;
private PrivilegedOperationExecutor privilegedOperationExecutor;
private Set<String> allowedNetworks = new HashSet<>();
private String defaultNetwork;
private String cgroupsRootDirectory;
private CGroupsHandler cGroupsHandler;
private AccessControlList privilegedContainersAcl;
private boolean enableUserReMapping;
private int userRemappingUidThreshold;
private int userRemappingGidThreshold;
private Set<String> capabilities;
/**
* Return whether the given environment variables indicate that the operation
* is requesting a Docker container. If the environment contains a key
* called {@code YARN_CONTAINER_RUNTIME_TYPE} whose value is {@code docker},
* this method will return true. Otherwise it will return false.
*
* @param env the environment variable settings for the operation
* @return whether a Docker container is requested
*/
public static boolean isDockerContainerRequested(
Map<String, String> env) {
if (env == null) {
return false;
}
String type = env.get(ContainerRuntimeConstants.ENV_CONTAINER_TYPE);
return type != null && type.equals("docker");
}
/**
* Create an instance using the given {@link PrivilegedOperationExecutor}
* instance for performing operations.
*
* @param privilegedOperationExecutor the {@link PrivilegedOperationExecutor}
* instance
*/
public DockerLinuxContainerRuntime(PrivilegedOperationExecutor
privilegedOperationExecutor) {
this(privilegedOperationExecutor,
ResourceHandlerModule.getCGroupsHandler());
}
/**
* Create an instance using the given {@link PrivilegedOperationExecutor}
* instance for performing operations and the given {@link CGroupsHandler}
* instance. This constructor is intended for use in testing.
*
* @param privilegedOperationExecutor the {@link PrivilegedOperationExecutor}
* instance
* @param cGroupsHandler the {@link CGroupsHandler} instance
*/
@VisibleForTesting
public DockerLinuxContainerRuntime(PrivilegedOperationExecutor
privilegedOperationExecutor, CGroupsHandler cGroupsHandler) {
this.privilegedOperationExecutor = privilegedOperationExecutor;
if (cGroupsHandler == null) {
LOG.info("cGroupsHandler is null - cgroups not in use.");
} else {
this.cGroupsHandler = cGroupsHandler;
this.cgroupsRootDirectory = cGroupsHandler.getCGroupMountPath();
}
}
@Override
public void initialize(Configuration conf)
throws ContainerExecutionException {
this.conf = conf;
dockerClient = new DockerClient(conf);
allowedNetworks.clear();
allowedNetworks.addAll(Arrays.asList(
conf.getTrimmedStrings(
YarnConfiguration.NM_DOCKER_ALLOWED_CONTAINER_NETWORKS,
YarnConfiguration.DEFAULT_NM_DOCKER_ALLOWED_CONTAINER_NETWORKS)));
defaultNetwork = conf.getTrimmed(
YarnConfiguration.NM_DOCKER_DEFAULT_CONTAINER_NETWORK,
YarnConfiguration.DEFAULT_NM_DOCKER_DEFAULT_CONTAINER_NETWORK);
if(!allowedNetworks.contains(defaultNetwork)) {
String message = "Default network: " + defaultNetwork
+ " is not in the set of allowed networks: " + allowedNetworks;
if (LOG.isWarnEnabled()) {
LOG.warn(message + ". Please check "
+ "configuration");
}
throw new ContainerExecutionException(message);
}
privilegedContainersAcl = new AccessControlList(conf.getTrimmed(
YarnConfiguration.NM_DOCKER_PRIVILEGED_CONTAINERS_ACL,
YarnConfiguration.DEFAULT_NM_DOCKER_PRIVILEGED_CONTAINERS_ACL));
enableUserReMapping = conf.getBoolean(
YarnConfiguration.NM_DOCKER_ENABLE_USER_REMAPPING,
YarnConfiguration.DEFAULT_NM_DOCKER_ENABLE_USER_REMAPPING);
userRemappingUidThreshold = conf.getInt(
YarnConfiguration.NM_DOCKER_USER_REMAPPING_UID_THRESHOLD,
YarnConfiguration.DEFAULT_NM_DOCKER_USER_REMAPPING_UID_THRESHOLD);
userRemappingGidThreshold = conf.getInt(
YarnConfiguration.NM_DOCKER_USER_REMAPPING_GID_THRESHOLD,
YarnConfiguration.DEFAULT_NM_DOCKER_USER_REMAPPING_GID_THRESHOLD);
capabilities = getDockerCapabilitiesFromConf();
}
private Set<String> getDockerCapabilitiesFromConf() throws
ContainerExecutionException {
Set<String> caps = new HashSet<>(Arrays.asList(
conf.getTrimmedStrings(
YarnConfiguration.NM_DOCKER_CONTAINER_CAPABILITIES,
YarnConfiguration.DEFAULT_NM_DOCKER_CONTAINER_CAPABILITIES)));
if(caps.contains("none") || caps.contains("NONE")) {
if(caps.size() > 1) {
String msg = "Mixing capabilities with the none keyword is" +
" not supported";
throw new ContainerExecutionException(msg);
}
caps = Collections.emptySet();
}
return caps;
}
public Set<String> getCapabilities() {
return capabilities;
}
@Override
public boolean useWhitelistEnv(Map<String, String> env) {
// Avoid propagating nodemanager environment variables into the container
// so those variables can be picked up from the Docker image instead.
return false;
}
@Override
public void prepareContainer(ContainerRuntimeContext ctx)
throws ContainerExecutionException {
}
private void validateContainerNetworkType(String network)
throws ContainerExecutionException {
if (allowedNetworks.contains(network)) {
return;
}
String msg = "Disallowed network: '" + network
+ "' specified. Allowed networks: are " + allowedNetworks
.toString();
throw new ContainerExecutionException(msg);
}
public static void validateHostname(String hostname) throws
ContainerExecutionException {
if (hostname != null && !hostname.isEmpty()) {
if (!hostnamePattern.matcher(hostname).matches()) {
throw new ContainerExecutionException("Hostname '" + hostname
+ "' doesn't match docker hostname pattern");
}
}
}
/** Set a DNS friendly hostname. */
private void setHostname(DockerRunCommand runCommand, String
containerIdStr, String name)
throws ContainerExecutionException {
if (name == null || name.isEmpty()) {
name = RegistryPathUtils.encodeYarnID(containerIdStr);
validateHostname(name);
}
LOG.info("setting hostname in container to: " + name);
runCommand.setHostname(name);
}
/**
* If CGROUPS in enabled and not set to none, then set the CGROUP parent for
* the command instance.
*
* @param resourcesOptions the resource options to check for "cgroups=none"
* @param containerIdStr the container ID
* @param runCommand the command to set with the CGROUP parent
*/
@VisibleForTesting
protected void addCGroupParentIfRequired(String resourcesOptions,
String containerIdStr, DockerRunCommand runCommand) {
if (cGroupsHandler == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("cGroupsHandler is null. cgroups are not in use. nothing to"
+ " do.");
}
return;
}
if (resourcesOptions.equals(PrivilegedOperation.CGROUP_ARG_PREFIX
+ PrivilegedOperation.CGROUP_ARG_NO_TASKS)) {
if (LOG.isDebugEnabled()) {
LOG.debug("no resource restrictions specified. not using docker's "
+ "cgroup options");
}
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("using docker's cgroups options");
}
String cGroupPath = "/"
+ cGroupsHandler.getRelativePathForCGroup(containerIdStr);
if (LOG.isDebugEnabled()) {
LOG.debug("using cgroup parent: " + cGroupPath);
}
runCommand.setCGroupParent(cGroupPath);
}
}
/**
* Return whether the YARN container is allowed to run in a privileged
* Docker container. For a privileged container to be allowed all of the
* following three conditions must be satisfied:
*
* <ol>
* <li>Submitting user must request for a privileged container</li>
* <li>Privileged containers must be enabled on the cluster</li>
* <li>Submitting user must be white-listed to run a privileged
* container</li>
* </ol>
*
* @param container the target YARN container
* @return whether privileged container execution is allowed
* @throws ContainerExecutionException if privileged container execution
* is requested but is not allowed
*/
private boolean allowPrivilegedContainerExecution(Container container)
throws ContainerExecutionException {
Map<String, String> environment = container.getLaunchContext()
.getEnvironment();
String runPrivilegedContainerEnvVar = environment
.get(ENV_DOCKER_CONTAINER_RUN_PRIVILEGED_CONTAINER);
if (runPrivilegedContainerEnvVar == null) {
return false;
}
if (!runPrivilegedContainerEnvVar.equalsIgnoreCase("true")) {
LOG.warn("NOT running a privileged container. Value of " +
ENV_DOCKER_CONTAINER_RUN_PRIVILEGED_CONTAINER
+ "is invalid: " + runPrivilegedContainerEnvVar);
return false;
}
LOG.info("Privileged container requested for : " + container
.getContainerId().toString());
//Ok, so we have been asked to run a privileged container. Security
// checks need to be run. Each violation is an error.
//check if privileged containers are enabled.
boolean privilegedContainersEnabledOnCluster = conf.getBoolean(
YarnConfiguration.NM_DOCKER_ALLOW_PRIVILEGED_CONTAINERS,
YarnConfiguration.DEFAULT_NM_DOCKER_ALLOW_PRIVILEGED_CONTAINERS);
if (!privilegedContainersEnabledOnCluster) {
String message = "Privileged container being requested but privileged "
+ "containers are not enabled on this cluster";
LOG.warn(message);
throw new ContainerExecutionException(message);
}
//check if submitting user is in the whitelist.
String submittingUser = container.getUser();
UserGroupInformation submitterUgi = UserGroupInformation
.createRemoteUser(submittingUser);
if (!privilegedContainersAcl.isUserAllowed(submitterUgi)) {
String message = "Cannot launch privileged container. Submitting user ("
+ submittingUser + ") fails ACL check.";
LOG.warn(message);
throw new ContainerExecutionException(message);
}
LOG.info("All checks pass. Launching privileged container for : "
+ container.getContainerId().toString());
return true;
}
@VisibleForTesting
protected String validateMount(String mount,
Map<Path, List<String>> localizedResources)
throws ContainerExecutionException {
for (Entry<Path, List<String>> resource : localizedResources.entrySet()) {
if (resource.getValue().contains(mount)) {
java.nio.file.Path path = Paths.get(resource.getKey().toString());
if (!path.isAbsolute()) {
throw new ContainerExecutionException("Mount must be absolute: " +
mount);
}
if (Files.isSymbolicLink(path)) {
throw new ContainerExecutionException("Mount cannot be a symlink: " +
mount);
}
return path.toString();
}
}
throw new ContainerExecutionException("Mount must be a localized " +
"resource: " + mount);
}
private String getUserIdInfo(String userName)
throws ContainerExecutionException {
String id = "";
Shell.ShellCommandExecutor shexec = new Shell.ShellCommandExecutor(
new String[]{"id", "-u", userName});
try {
shexec.execute();
id = shexec.getOutput().replaceAll("[^0-9]", "");
} catch (Exception e) {
throw new ContainerExecutionException(e);
}
return id;
}
private String[] getGroupIdInfo(String userName)
throws ContainerExecutionException {
String[] id = null;
Shell.ShellCommandExecutor shexec = new Shell.ShellCommandExecutor(
new String[]{"id", "-G", userName});
try {
shexec.execute();
id = shexec.getOutput().replace("\n", "").split(" ");
} catch (Exception e) {
throw new ContainerExecutionException(e);
}
return id;
}
@Override
public void launchContainer(ContainerRuntimeContext ctx)
throws ContainerExecutionException {
Container container = ctx.getContainer();
Map<String, String> environment = container.getLaunchContext()
.getEnvironment();
String imageName = environment.get(ENV_DOCKER_CONTAINER_IMAGE);
String network = environment.get(ENV_DOCKER_CONTAINER_NETWORK);
String hostname = environment.get(ENV_DOCKER_CONTAINER_HOSTNAME);
if(network == null || network.isEmpty()) {
network = defaultNetwork;
}
validateContainerNetworkType(network);
validateHostname(hostname);
validateImageName(imageName);
String containerIdStr = container.getContainerId().toString();
String runAsUser = ctx.getExecutionAttribute(RUN_AS_USER);
String dockerRunAsUser = runAsUser;
Path containerWorkDir = ctx.getExecutionAttribute(CONTAINER_WORK_DIR);
String[] groups = null;
if (enableUserReMapping) {
String uid = getUserIdInfo(runAsUser);
groups = getGroupIdInfo(runAsUser);
String gid = groups[0];
if(Integer.parseInt(uid) < userRemappingUidThreshold) {
String message = "uid: " + uid + " below threshold: "
+ userRemappingUidThreshold;
throw new ContainerExecutionException(message);
}
for(int i = 0; i < groups.length; i++) {
String group = groups[i];
if (Integer.parseInt(group) < userRemappingGidThreshold) {
String message = "gid: " + group
+ " below threshold: " + userRemappingGidThreshold;
throw new ContainerExecutionException(message);
}
}
dockerRunAsUser = uid + ":" + gid;
}
//List<String> -> stored as List -> fetched/converted to List<String>
//we can't do better here thanks to type-erasure
@SuppressWarnings("unchecked")
List<String> filecacheDirs = ctx.getExecutionAttribute(FILECACHE_DIRS);
@SuppressWarnings("unchecked")
List<String> containerLocalDirs = ctx.getExecutionAttribute(
CONTAINER_LOCAL_DIRS);
@SuppressWarnings("unchecked")
List<String> containerLogDirs = ctx.getExecutionAttribute(
CONTAINER_LOG_DIRS);
@SuppressWarnings("unchecked")
Map<Path, List<String>> localizedResources = ctx.getExecutionAttribute(
LOCALIZED_RESOURCES);
@SuppressWarnings("unchecked")
List<String> userLocalDirs = ctx.getExecutionAttribute(USER_LOCAL_DIRS);
@SuppressWarnings("unchecked")
DockerRunCommand runCommand = new DockerRunCommand(containerIdStr,
dockerRunAsUser, imageName)
.detachOnRun()
.setContainerWorkDir(containerWorkDir.toString())
.setNetworkType(network);
setHostname(runCommand, containerIdStr, hostname);
runCommand.setCapabilities(capabilities);
if(cgroupsRootDirectory != null) {
runCommand.addReadOnlyMountLocation(cgroupsRootDirectory,
cgroupsRootDirectory, false);
}
List<String> allDirs = new ArrayList<>(containerLocalDirs);
allDirs.addAll(filecacheDirs);
allDirs.add(containerWorkDir.toString());
allDirs.addAll(containerLogDirs);
allDirs.addAll(userLocalDirs);
for (String dir: allDirs) {
runCommand.addMountLocation(dir, dir, true);
}
if (environment.containsKey(ENV_DOCKER_CONTAINER_LOCAL_RESOURCE_MOUNTS)) {
String mounts = environment.get(
ENV_DOCKER_CONTAINER_LOCAL_RESOURCE_MOUNTS);
if (!mounts.isEmpty()) {
for (String mount : StringUtils.split(mounts)) {
String[] dir = StringUtils.split(mount, ':');
if (dir.length != 2) {
throw new ContainerExecutionException("Invalid mount : " +
mount);
}
String src = validateMount(dir[0], localizedResources);
String dst = dir[1];
runCommand.addReadOnlyMountLocation(src, dst, true);
}
}
}
if (allowPrivilegedContainerExecution(container)) {
runCommand.setPrivileged();
}
String resourcesOpts = ctx.getExecutionAttribute(RESOURCES_OPTIONS);
addCGroupParentIfRequired(resourcesOpts, containerIdStr, runCommand);
String disableOverride = environment.get(
ENV_DOCKER_CONTAINER_RUN_OVERRIDE_DISABLE);
if (disableOverride != null && disableOverride.equals("true")) {
LOG.info("command override disabled");
} else {
List<String> overrideCommands = new ArrayList<>();
Path launchDst =
new Path(containerWorkDir, ContainerLaunch.CONTAINER_SCRIPT);
overrideCommands.add("bash");
overrideCommands.add(launchDst.toUri().getPath());
runCommand.setOverrideCommandWithArgs(overrideCommands);
}
if(enableUserReMapping) {
runCommand.groupAdd(groups);
}
String commandFile = dockerClient.writeCommandToTempFile(runCommand,
containerIdStr);
PrivilegedOperation launchOp = buildLaunchOp(ctx,
commandFile, runCommand);
try {
privilegedOperationExecutor.executePrivilegedOperation(null,
launchOp, null, null, false, false);
} catch (PrivilegedOperationException e) {
LOG.warn("Launch container failed. Exception: ", e);
LOG.info("Docker command used: " + runCommand);
throw new ContainerExecutionException("Launch container failed", e
.getExitCode(), e.getOutput(), e.getErrorOutput());
}
}
@Override
public void signalContainer(ContainerRuntimeContext ctx)
throws ContainerExecutionException {
ContainerExecutor.Signal signal = ctx.getExecutionAttribute(SIGNAL);
PrivilegedOperation privOp = null;
// Handle liveliness checks, send null signal to pid
if(ContainerExecutor.Signal.NULL.equals(signal)) {
privOp = new PrivilegedOperation(
PrivilegedOperation.OperationType.SIGNAL_CONTAINER);
privOp.appendArgs(ctx.getExecutionAttribute(RUN_AS_USER),
ctx.getExecutionAttribute(USER),
Integer.toString(PrivilegedOperation.RunAsUserCommand
.SIGNAL_CONTAINER.getValue()),
ctx.getExecutionAttribute(PID),
Integer.toString(ctx.getExecutionAttribute(SIGNAL).getValue()));
// All other signals handled as docker stop
} else {
String containerId = ctx.getContainer().getContainerId().toString();
DockerStopCommand stopCommand = new DockerStopCommand(containerId);
String commandFile = dockerClient.writeCommandToTempFile(stopCommand,
containerId);
privOp = new PrivilegedOperation(
PrivilegedOperation.OperationType.RUN_DOCKER_CMD);
privOp.appendArgs(commandFile);
}
//Some failures here are acceptable. Let the calling executor decide.
privOp.disableFailureLogging();
try {
privilegedOperationExecutor.executePrivilegedOperation(null,
privOp, null, null, false, false);
} catch (PrivilegedOperationException e) {
throw new ContainerExecutionException("Signal container failed", e
.getExitCode(), e.getOutput(), e.getErrorOutput());
}
}
@Override
public void reapContainer(ContainerRuntimeContext ctx)
throws ContainerExecutionException {
}
// ipAndHost[0] contains comma separated list of IPs
// ipAndHost[1] contains the hostname.
@Override
public String[] getIpAndHost(Container container) {
String containerId = container.getContainerId().toString();
DockerInspectCommand inspectCommand =
new DockerInspectCommand(containerId).getIpAndHost();
try {
String commandFile = dockerClient.writeCommandToTempFile(inspectCommand,
containerId);
PrivilegedOperation privOp = new PrivilegedOperation(
PrivilegedOperation.OperationType.RUN_DOCKER_CMD);
privOp.appendArgs(commandFile);
String output = privilegedOperationExecutor
.executePrivilegedOperation(null, privOp, null,
null, true, false);
LOG.info("Docker inspect output for " + containerId + ": " + output);
int index = output.lastIndexOf(',');
if (index == -1) {
LOG.error("Incorrect format for ip and host");
return null;
}
String ips = output.substring(0, index).trim();
String host = output.substring(index+1).trim();
String[] ipAndHost = new String[2];
ipAndHost[0] = ips;
ipAndHost[1] = host;
return ipAndHost;
} catch (ContainerExecutionException e) {
LOG.error("Error when writing command to temp file", e);
} catch (PrivilegedOperationException e) {
LOG.error("Error when executing command.", e);
}
return null;
}
private PrivilegedOperation buildLaunchOp(ContainerRuntimeContext ctx,
String commandFile, DockerRunCommand runCommand) {
String runAsUser = ctx.getExecutionAttribute(RUN_AS_USER);
String containerIdStr = ctx.getContainer().getContainerId().toString();
Path nmPrivateContainerScriptPath = ctx.getExecutionAttribute(
NM_PRIVATE_CONTAINER_SCRIPT_PATH);
Path containerWorkDir = ctx.getExecutionAttribute(CONTAINER_WORK_DIR);
//we can't do better here thanks to type-erasure
@SuppressWarnings("unchecked")
List<String> localDirs = ctx.getExecutionAttribute(LOCAL_DIRS);
@SuppressWarnings("unchecked")
List<String> logDirs = ctx.getExecutionAttribute(LOG_DIRS);
String resourcesOpts = ctx.getExecutionAttribute(RESOURCES_OPTIONS);
PrivilegedOperation launchOp = new PrivilegedOperation(
PrivilegedOperation.OperationType.LAUNCH_DOCKER_CONTAINER);
launchOp.appendArgs(runAsUser, ctx.getExecutionAttribute(USER),
Integer.toString(PrivilegedOperation
.RunAsUserCommand.LAUNCH_DOCKER_CONTAINER.getValue()),
ctx.getExecutionAttribute(APPID),
containerIdStr,
containerWorkDir.toString(),
nmPrivateContainerScriptPath.toUri().getPath(),
ctx.getExecutionAttribute(NM_PRIVATE_TOKENS_PATH).toUri().getPath(),
ctx.getExecutionAttribute(PID_FILE_PATH).toString(),
StringUtils.join(PrivilegedOperation.LINUX_FILE_PATH_SEPARATOR,
localDirs),
StringUtils.join(PrivilegedOperation.LINUX_FILE_PATH_SEPARATOR,
logDirs),
commandFile,
resourcesOpts);
String tcCommandFile = ctx.getExecutionAttribute(TC_COMMAND_FILE);
if (tcCommandFile != null) {
launchOp.appendArgs(tcCommandFile);
}
if (LOG.isDebugEnabled()) {
LOG.debug("Launching container with cmd: " + runCommand);
}
return launchOp;
}
public static void validateImageName(String imageName)
throws ContainerExecutionException {
if (imageName == null || imageName.isEmpty()) {
throw new ContainerExecutionException(
ENV_DOCKER_CONTAINER_IMAGE + " not set!");
}
if (!dockerImagePattern.matcher(imageName).matches()) {
throw new ContainerExecutionException("Image name '" + imageName
+ "' doesn't match docker image name pattern");
}
}
}