YARN-5428. Allow for specifying the docker client configuration directory. Contributed by Shane Kumpf

This commit is contained in:
Jian He 2018-02-07 10:59:38 -08:00
parent 996796f104
commit eb2449d539
11 changed files with 690 additions and 10 deletions

View File

@ -87,6 +87,7 @@
import org.apache.hadoop.yarn.exceptions.ResourceNotFoundException;
import org.apache.hadoop.yarn.exceptions.YARNFeatureNotEnabledException;
import org.apache.hadoop.yarn.exceptions.YarnException;
import org.apache.hadoop.yarn.util.DockerClientConfigHandler;
import org.apache.hadoop.yarn.util.UnitsConversionUtil;
import org.apache.hadoop.yarn.util.resource.ResourceUtils;
import org.apache.hadoop.yarn.util.resource.Resources;
@ -225,6 +226,9 @@ public class Client {
private String flowVersion = null;
private long flowRunId = 0L;
// Docker client configuration
private String dockerClientConfig = null;
// Command line options
private Options opts;
@ -368,6 +372,10 @@ public Client(Configuration conf) throws Exception {
"If container could retry, it specifies max retires");
opts.addOption("container_retry_interval", true,
"Interval between each retry, unit is milliseconds");
opts.addOption("docker_client_config", true,
"The docker client configuration path. The scheme should be supplied"
+ " (i.e. file:// or hdfs://)."
+ " Only used when the Docker runtime is enabled and requested.");
opts.addOption("placement_spec", true,
"Placement specification. Please note, if this option is specified,"
+ " The \"num_containers\" option will be ignored. All requested"
@ -585,6 +593,9 @@ public boolean init(String[] args) throws ParseException {
"Flow run is not a valid long value", e);
}
}
if (cliParser.hasOption("docker_client_config")) {
dockerClientConfig = cliParser.getOptionValue("docker_client_config");
}
return true;
}
@ -884,9 +895,10 @@ public boolean run() throws IOException, YarnException {
// amContainer.setServiceData(serviceData);
// Setup security tokens
Credentials rmCredentials = null;
if (UserGroupInformation.isSecurityEnabled()) {
// Note: Credentials class is marked as LimitedPrivate for HDFS and MapReduce
Credentials credentials = new Credentials();
rmCredentials = new Credentials();
String tokenRenewer = YarnClientUtils.getRmPrincipal(conf);
if (tokenRenewer == null || tokenRenewer.length() == 0) {
throw new IOException(
@ -895,16 +907,32 @@ public boolean run() throws IOException, YarnException {
// For now, only getting tokens for the default file-system.
final Token<?> tokens[] =
fs.addDelegationTokens(tokenRenewer, credentials);
fs.addDelegationTokens(tokenRenewer, rmCredentials);
if (tokens != null) {
for (Token<?> token : tokens) {
LOG.info("Got dt for " + fs.getUri() + "; " + token);
}
}
}
// Add the docker client config credentials if supplied.
Credentials dockerCredentials = null;
if (dockerClientConfig != null) {
dockerCredentials =
DockerClientConfigHandler.readCredentialsFromConfigFile(
new Path(dockerClientConfig), conf, appId.toString());
}
if (rmCredentials != null || dockerCredentials != null) {
DataOutputBuffer dob = new DataOutputBuffer();
credentials.writeTokenStorageToStream(dob);
ByteBuffer fsTokens = ByteBuffer.wrap(dob.getData(), 0, dob.getLength());
amContainer.setTokens(fsTokens);
if (rmCredentials != null) {
rmCredentials.writeTokenStorageToStream(dob);
}
if (dockerCredentials != null) {
dockerCredentials.writeTokenStorageToStream(dob);
}
ByteBuffer tokens = ByteBuffer.wrap(dob.getData(), 0, dob.getLength());
amContainer.setTokens(tokens);
}
appContext.setAMContainerSpec(amContainer);

View File

@ -0,0 +1,159 @@
/*
* 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.security;
import com.google.protobuf.TextFormat;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.token.TokenIdentifier;
import org.apache.hadoop.yarn.proto.YarnSecurityTokenProtos.DockerCredentialTokenIdentifierProto;
import org.slf4j.LoggerFactory;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.IOException;
/**
* TokenIdentifier for Docker registry credentials.
*/
public class DockerCredentialTokenIdentifier extends TokenIdentifier {
private static final org.slf4j.Logger LOG =
LoggerFactory.getLogger(DockerCredentialTokenIdentifier.class);
private DockerCredentialTokenIdentifierProto proto;
public static final Text KIND = new Text("DOCKER_CLIENT_CREDENTIAL_TOKEN");
public DockerCredentialTokenIdentifier(String registryUrl,
String applicationId) {
DockerCredentialTokenIdentifierProto.Builder builder =
DockerCredentialTokenIdentifierProto.newBuilder();
if (registryUrl != null) {
builder.setRegistryUrl(registryUrl);
}
if (applicationId != null) {
builder.setApplicationId(applicationId);
}
proto = builder.build();
}
/**
* Default constructor needed for the Service Loader.
*/
public DockerCredentialTokenIdentifier() {
}
/**
* Write the TokenIdentifier to the output stream.
*
* @param out <code>DataOutput</code> to serialize this object into.
* @throws IOException if the write fails.
*/
@Override
public void write(DataOutput out) throws IOException {
out.write(proto.toByteArray());
}
/**
* Populate the Proto object with the input.
*
* @param in <code>DataInput</code> to deserialize this object from.
* @throws IOException if the read fails.
*/
@Override
public void readFields(DataInput in) throws IOException {
proto = DockerCredentialTokenIdentifierProto.parseFrom((DataInputStream)in);
}
/**
* Return the ProtoBuf formatted data.
*
* @return the ProtoBuf representation of the data.
*/
public DockerCredentialTokenIdentifierProto getProto() {
return proto;
}
/**
* Return the TokenIdentifier kind.
*
* @return the TokenIdentifier kind.
*/
@Override
public Text getKind() {
return KIND;
}
/**
* Return a remote user based on the registry URL and Application ID.
*
* @return a remote user based on the registry URL and Application ID.
*/
@Override
public UserGroupInformation getUser() {
return UserGroupInformation.createRemoteUser(
getRegistryUrl() + "-" + getApplicationId());
}
/**
* Get the registry URL.
*
* @return the registry URL.
*/
public String getRegistryUrl() {
String registryUrl = null;
if (proto.hasRegistryUrl()) {
registryUrl = proto.getRegistryUrl();
}
return registryUrl;
}
/**
* Get the application ID.
*
* @return the application ID.
*/
public String getApplicationId() {
String applicationId = null;
if (proto.hasApplicationId()) {
applicationId = proto.getApplicationId();
}
return applicationId;
}
@Override
public int hashCode() {
return getProto().hashCode();
}
@Override
public boolean equals(Object other) {
if (other == null) {
return false;
}
if (other.getClass().isAssignableFrom(this.getClass())) {
return this.getProto().equals(this.getClass().cast(other).getProto());
}
return false;
}
@Override
public String toString() {
return TextFormat.shortDebugString(getProto());
}
}

View File

@ -0,0 +1,183 @@
/*
* 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.util;
import org.apache.commons.io.FileUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.commons.io.IOUtils;
import org.apache.hadoop.io.DataInputByteBuffer;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.security.Credentials;
import org.apache.hadoop.security.token.Token;
import org.apache.hadoop.security.token.TokenIdentifier;
import org.apache.hadoop.yarn.security.DockerCredentialTokenIdentifier;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.ObjectNode;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Iterator;
/**
* Commonly needed actions for handling the Docker client configurations.
*
* Credentials that are used to access private Docker registries are supplied.
* Actions include:
* <ul>
* <li>Read the Docker client configuration json file from a
* {@link FileSystem}.</li>
* <li>Extract the authentication information from the configuration into
* {@link Token} and {@link Credentials} objects.</li>
* <li>Tokens are commonly shipped via the
* {@link org.apache.hadoop.yarn.api.records.ContainerLaunchContext} as a
* {@link ByteBuffer}, extract the {@link Credentials}.</li>
* <li>Write the Docker client configuration json back to the local filesystem
* to be used by the Docker command line.</li>
* </ul>
*/
public final class DockerClientConfigHandler {
private static final org.slf4j.Logger LOG =
LoggerFactory.getLogger(DockerClientConfigHandler.class);
private static final String CONFIG_AUTHS_KEY = "auths";
private static final String CONFIG_AUTH_KEY = "auth";
private DockerClientConfigHandler() { }
/**
* Read the Docker client configuration and extract the auth tokens into
* Credentials.
*
* @param configFile the Path to the Docker client configuration.
* @param conf the Configuration object, needed by the FileSystem.
* @param applicationId the application ID to associate the Credentials with.
* @return the populated Credential object with the Docker Tokens.
* @throws IOException if the file can not be read.
*/
public static Credentials readCredentialsFromConfigFile(Path configFile,
Configuration conf, String applicationId) throws IOException {
// Read the config file
String contents = null;
configFile = new Path(configFile.toUri());
FileSystem fs = configFile.getFileSystem(conf);
if (fs != null) {
FSDataInputStream fileHandle = fs.open(configFile);
if (fileHandle != null) {
contents = IOUtils.toString(fileHandle);
}
}
if (contents == null) {
throw new IOException("Failed to read Docker client configuration: "
+ configFile);
}
// Parse the JSON and create the Tokens/Credentials.
ObjectMapper mapper = new ObjectMapper();
JsonFactory factory = mapper.getJsonFactory();
JsonParser parser = factory.createJsonParser(contents);
JsonNode rootNode = mapper.readTree(parser);
Credentials credentials = new Credentials();
if (rootNode.has(CONFIG_AUTHS_KEY)) {
Iterator<String> iter = rootNode.get(CONFIG_AUTHS_KEY).getFieldNames();
for (; iter.hasNext();) {
String registryUrl = iter.next();
String registryCred = rootNode.get(CONFIG_AUTHS_KEY)
.get(registryUrl)
.get(CONFIG_AUTH_KEY)
.asText();
TokenIdentifier tokenId =
new DockerCredentialTokenIdentifier(registryUrl, applicationId);
Token<DockerCredentialTokenIdentifier> token =
new Token<>(tokenId.getBytes(),
registryCred.getBytes(Charset.forName("UTF-8")),
tokenId.getKind(), new Text(registryUrl));
credentials.addToken(
new Text(registryUrl + "-" + applicationId), token);
if (LOG.isDebugEnabled()) {
LOG.debug("Added token: " + token.toString());
}
}
}
return credentials;
}
/**
* Convert the Token ByteBuffer to the appropriate Credentials object.
*
* @param tokens the Tokens from the ContainerLaunchContext.
* @return the Credentials object populated from the Tokens.
*/
public static Credentials getCredentialsFromTokensByteBuffer(
ByteBuffer tokens) throws IOException {
Credentials credentials = new Credentials();
DataInputByteBuffer dibb = new DataInputByteBuffer();
tokens.rewind();
dibb.reset(tokens);
credentials.readTokenStorageStream(dibb);
tokens.rewind();
if (LOG.isDebugEnabled()) {
for (Token token : credentials.getAllTokens()) {
LOG.debug("Added token: " + token.toString());
}
}
return credentials;
}
/**
* Extract the Docker related tokens from the Credentials and write the Docker
* client configuration to the supplied File.
*
* @param outConfigFile the File to write the Docker client configuration to.
* @param credentials the populated Credentials object.
* @throws IOException if the write fails.
*/
public static void writeDockerCredentialsToPath(File outConfigFile,
Credentials credentials) throws IOException {
ObjectMapper mapper = new ObjectMapper();
ObjectNode rootNode = mapper.createObjectNode();
ObjectNode registryUrlNode = mapper.createObjectNode();
if (credentials.numberOfTokens() > 0) {
for (Token<? extends TokenIdentifier> tk : credentials.getAllTokens()) {
if (tk.getKind().equals(DockerCredentialTokenIdentifier.KIND)) {
DockerCredentialTokenIdentifier ti =
(DockerCredentialTokenIdentifier) tk.decodeIdentifier();
ObjectNode registryCredNode = mapper.createObjectNode();
registryUrlNode.put(ti.getRegistryUrl(), registryCredNode);
registryCredNode.put(CONFIG_AUTH_KEY,
new String(tk.getPassword(), Charset.forName("UTF-8")));
if (LOG.isDebugEnabled()) {
LOG.debug("Prepared token for write: " + tk.toString());
}
}
}
}
rootNode.put(CONFIG_AUTHS_KEY, registryUrlNode);
String json =
mapper.writerWithDefaultPrettyPrinter().writeValueAsString(rootNode);
FileUtils.writeStringToFile(outConfigFile, json, Charset.defaultCharset());
}
}

View File

@ -72,3 +72,8 @@ message YARNDelegationTokenIdentifierProto {
optional int32 masterKeyId = 7;
}
message DockerCredentialTokenIdentifierProto {
optional string registryUrl = 1;
optional string applicationId = 2;
}

View File

@ -17,3 +17,4 @@ org.apache.hadoop.yarn.security.client.ClientToAMTokenIdentifier
org.apache.hadoop.yarn.security.client.RMDelegationTokenIdentifier
org.apache.hadoop.yarn.security.client.TimelineDelegationTokenIdentifier
org.apache.hadoop.yarn.security.NMTokenIdentifier
org.apache.hadoop.yarn.security.DockerCredentialTokenIdentifier

View File

@ -0,0 +1,129 @@
/*
* 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.security;
import org.apache.commons.io.FileUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.DataOutputBuffer;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.security.Credentials;
import org.apache.hadoop.security.token.Token;
import org.apache.hadoop.security.token.TokenIdentifier;
import org.apache.hadoop.yarn.util.DockerClientConfigHandler;
import org.junit.Before;
import org.junit.Test;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.nio.ByteBuffer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* Test the functionality of the DockerClientConfigHandler.
*/
public class TestDockerClientConfigHandler {
public static final String JSON = "{\"auths\": "
+ "{\"https://index.docker.io/v1/\": "
+ "{\"auth\": \"foobarbaz\"},"
+ "\"registry.example.com\": "
+ "{\"auth\": \"bazbarfoo\"}}}";
private static final String APPLICATION_ID = "application_2313_2131341";
private File file;
private Configuration conf = new Configuration();
@Before
public void setUp() throws Exception {
file = File.createTempFile("docker-client-config", "test");
file.deleteOnExit();
BufferedWriter bw = new BufferedWriter(new FileWriter(file));
bw.write(JSON);
bw.close();
}
@Test
public void testReadCredentialsFromConfigFile() throws Exception {
Credentials credentials =
DockerClientConfigHandler.readCredentialsFromConfigFile(
new Path(file.toURI()), conf, APPLICATION_ID);
Token token1 = credentials.getToken(
new Text("https://index.docker.io/v1/-" + APPLICATION_ID));
assertEquals(DockerCredentialTokenIdentifier.KIND, token1.getKind());
assertEquals("foobarbaz", new String(token1.getPassword()));
DockerCredentialTokenIdentifier ti1 =
(DockerCredentialTokenIdentifier) token1.decodeIdentifier();
assertEquals("https://index.docker.io/v1/", ti1.getRegistryUrl());
assertEquals(APPLICATION_ID, ti1.getApplicationId());
Token token2 = credentials.getToken(
new Text("registry.example.com-" + APPLICATION_ID));
assertEquals(DockerCredentialTokenIdentifier.KIND, token2.getKind());
assertEquals("bazbarfoo", new String(token2.getPassword()));
DockerCredentialTokenIdentifier ti2 =
(DockerCredentialTokenIdentifier) token2.decodeIdentifier();
assertEquals("registry.example.com", ti2.getRegistryUrl());
assertEquals(APPLICATION_ID, ti2.getApplicationId());
}
@Test
public void testGetCredentialsFromTokensByteBuffer() throws Exception {
Credentials credentials =
DockerClientConfigHandler.readCredentialsFromConfigFile(
new Path(file.toURI()), conf, APPLICATION_ID);
DataOutputBuffer dob = new DataOutputBuffer();
credentials.writeTokenStorageToStream(dob);
ByteBuffer tokens = ByteBuffer.wrap(dob.getData(), 0, dob.getLength());
Credentials credentialsOut =
DockerClientConfigHandler.getCredentialsFromTokensByteBuffer(tokens);
assertEquals(credentials.numberOfTokens(), credentialsOut.numberOfTokens());
for (Token<? extends TokenIdentifier> tkIn : credentials.getAllTokens()) {
DockerCredentialTokenIdentifier ti =
(DockerCredentialTokenIdentifier) tkIn.decodeIdentifier();
Token tkOut = credentialsOut.getToken(
new Text(ti.getRegistryUrl() + "-" + ti.getApplicationId()));
assertEquals(tkIn.getKind(), tkOut.getKind());
assertEquals(new String(tkIn.getIdentifier()),
new String(tkOut.getIdentifier()));
assertEquals(new String(tkIn.getPassword()),
new String(tkOut.getPassword()));
assertEquals(tkIn.getService(), tkOut.getService());
}
}
@Test
public void testWriteDockerCredentialsToPath() throws Exception {
File outFile = File.createTempFile("docker-client-config", "out");
outFile.deleteOnExit();
Credentials credentials =
DockerClientConfigHandler.readCredentialsFromConfigFile(
new Path(file.toURI()), conf, APPLICATION_ID);
DockerClientConfigHandler.writeDockerCredentialsToPath(outFile,
credentials);
assertTrue(outFile.exists());
String fileContents = FileUtils.readFileToString(outFile);
assertTrue(fileContents.contains("auths"));
assertTrue(fileContents.contains("registry.example.com"));
assertTrue(fileContents.contains("https://index.docker.io/v1/"));
assertTrue(fileContents.contains("foobarbaz"));
assertTrue(fileContents.contains("bazbarfoo"));
}
}

View File

@ -21,6 +21,7 @@
package org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.runtime;
import com.google.common.annotations.VisibleForTesting;
import org.apache.hadoop.security.Credentials;
import org.apache.hadoop.yarn.server.nodemanager.Context;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.runtime.docker.DockerCommandExecutor;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.runtime.docker.DockerKillCommand;
@ -28,6 +29,7 @@
import org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.runtime.docker.DockerVolumeCommand;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.resourceplugin.DockerCommandPlugin;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.resourceplugin.ResourcePlugin;
import org.apache.hadoop.yarn.util.DockerClientConfigHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.hadoop.classification.InterfaceAudience;
@ -58,8 +60,11 @@
import org.apache.hadoop.yarn.server.nodemanager.containermanager.runtime.ContainerRuntimeConstants;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.runtime.ContainerRuntimeContext;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
@ -846,6 +851,8 @@ public void launchContainer(ContainerRuntimeContext ctx)
runCommand.setPrivileged();
}
addDockerClientConfigToRunCommand(ctx, runCommand);
String resourcesOpts = ctx.getExecutionAttribute(RESOURCES_OPTIONS);
addCGroupParentIfRequired(resourcesOpts, containerIdStr, runCommand);
@ -1181,4 +1188,36 @@ private void handleContainerRemove(String containerId,
}
}
}
private void addDockerClientConfigToRunCommand(ContainerRuntimeContext ctx,
DockerRunCommand dockerRunCommand) throws ContainerExecutionException {
ByteBuffer tokens = ctx.getContainer().getLaunchContext().getTokens();
Credentials credentials;
if (tokens != null) {
tokens.rewind();
if (tokens.hasRemaining()) {
try {
credentials = DockerClientConfigHandler
.getCredentialsFromTokensByteBuffer(tokens);
} catch (IOException e) {
throw new ContainerExecutionException("Unable to read tokens.");
}
if (credentials.numberOfTokens() > 0) {
Path nmPrivateDir =
ctx.getExecutionAttribute(NM_PRIVATE_CONTAINER_SCRIPT_PATH)
.getParent();
File dockerConfigPath = new File(nmPrivateDir + "/config.json");
try {
DockerClientConfigHandler
.writeDockerCredentialsToPath(dockerConfigPath, credentials);
} catch (IOException e) {
throw new ContainerExecutionException(
"Unable to write Docker client credentials to "
+ dockerConfigPath);
}
dockerRunCommand.setClientConfigDir(dockerConfigPath.getParent());
}
}
}
}
}

View File

@ -88,4 +88,20 @@ public String toString() {
}
return ret.toString();
}
/**
* Add the client configuration directory to the docker command.
*
* The client configuration option proceeds any of the docker subcommands
* (such as run, load, pull, etc). Ordering will be handled by
* container-executor. Docker expects the value to be a directory containing
* the file config.json. This file is typically generated via docker login.
*
* @param clientConfigDir - directory containing the docker client config.
*/
public void setClientConfigDir(String clientConfigDir) {
if (clientConfigDir != null) {
addCommandArguments("docker-config", clientConfigDir);
}
}
}

View File

@ -24,11 +24,16 @@
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileUtil;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.DataOutputBuffer;
import org.apache.hadoop.registry.client.binding.RegistryPathUtils;
import org.apache.hadoop.security.Credentials;
import org.apache.hadoop.util.Shell;
import org.apache.hadoop.util.StringUtils;
import org.apache.hadoop.yarn.api.records.ContainerId;
import org.apache.hadoop.yarn.api.records.ContainerLaunchContext;
import org.apache.hadoop.yarn.util.DockerClientConfigHandler;
import org.apache.hadoop.yarn.conf.YarnConfiguration;
import org.apache.hadoop.yarn.security.TestDockerClientConfigHandler;
import org.apache.hadoop.yarn.server.nodemanager.ContainerExecutor;
import org.apache.hadoop.yarn.server.nodemanager.Context;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.container.Container;
@ -56,12 +61,18 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@ -69,6 +80,7 @@
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import static org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.runtime.LinuxContainerRuntimeConstants.APPID;
import static org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.runtime.LinuxContainerRuntimeConstants.APPLICATION_LOCAL_DIRS;
@ -1700,6 +1712,103 @@ public void testDockerCapabilities() throws ContainerExecutionException {
Assert.assertEquals("DAC_OVERRIDE", it.next());
}
@Test
public void testLaunchContainerWithDockerTokens()
throws ContainerExecutionException, PrivilegedOperationException,
IOException {
// Write the JSOn to a temp file.
File file = File.createTempFile("docker-client-config", "runtime-test");
file.deleteOnExit();
BufferedWriter bw = new BufferedWriter(new FileWriter(file));
bw.write(TestDockerClientConfigHandler.JSON);
bw.close();
// Get the credentials object with the Tokens.
Credentials credentials = DockerClientConfigHandler
.readCredentialsFromConfigFile(new Path(file.toURI()), conf, appId);
DataOutputBuffer dob = new DataOutputBuffer();
credentials.writeTokenStorageToStream(dob);
ByteBuffer tokens = ByteBuffer.wrap(dob.getData(), 0, dob.getLength());
// Configure the runtime and launch the container
when(context.getTokens()).thenReturn(tokens);
DockerLinuxContainerRuntime runtime =
new DockerLinuxContainerRuntime(mockExecutor, mockCGroupsHandler);
runtime.initialize(conf, null);
Set<PosixFilePermission> perms =
PosixFilePermissions.fromString("rwxr-xr--");
FileAttribute<Set<PosixFilePermission>> attr =
PosixFilePermissions.asFileAttribute(perms);
Path outDir = new Path(
Files.createTempDirectory("docker-client-config-out", attr).toUri()
.getPath() + "/launch_container.sh");
builder.setExecutionAttribute(NM_PRIVATE_CONTAINER_SCRIPT_PATH, outDir);
runtime.launchContainer(builder.build());
PrivilegedOperation op = capturePrivilegedOperation();
Assert.assertEquals(
PrivilegedOperation.OperationType.LAUNCH_DOCKER_CONTAINER,
op.getOperationType());
List<String> args = op.getArguments();
int expectedArgs = 13;
int argsCounter = 0;
Assert.assertEquals(expectedArgs, args.size());
Assert.assertEquals(runAsUser, args.get(argsCounter++));
Assert.assertEquals(user, args.get(argsCounter++));
Assert.assertEquals(Integer.toString(
PrivilegedOperation.RunAsUserCommand.LAUNCH_DOCKER_CONTAINER
.getValue()), args.get(argsCounter++));
Assert.assertEquals(appId, args.get(argsCounter++));
Assert.assertEquals(containerId, args.get(argsCounter++));
Assert.assertEquals(containerWorkDir.toString(), args.get(argsCounter++));
Assert.assertEquals(outDir.toUri().getPath(), args.get(argsCounter++));
Assert.assertEquals(nmPrivateTokensPath.toUri().getPath(),
args.get(argsCounter++));
Assert.assertEquals(pidFilePath.toString(), args.get(argsCounter++));
Assert.assertEquals(localDirs.get(0), args.get(argsCounter++));
Assert.assertEquals(logDirs.get(0), args.get(argsCounter++));
String dockerCommandFile = args.get(argsCounter++);
Assert.assertEquals(resourcesOptions, args.get(argsCounter));
List<String> dockerCommands = Files
.readAllLines(Paths.get(dockerCommandFile), Charset.forName("UTF-8"));
int expected = 15;
int counter = 0;
Assert.assertEquals(expected, dockerCommands.size());
Assert.assertEquals("[docker-command-execution]",
dockerCommands.get(counter++));
Assert.assertEquals(" cap-add=SYS_CHROOT,NET_BIND_SERVICE",
dockerCommands.get(counter++));
Assert.assertEquals(" cap-drop=ALL", dockerCommands.get(counter++));
Assert.assertEquals(" detach=true", dockerCommands.get(counter++));
Assert.assertEquals(" docker-command=run", dockerCommands.get(counter++));
Assert.assertEquals(" docker-config=" + outDir.getParent(),
dockerCommands.get(counter++));
Assert.assertEquals(" group-add=" + String.join(",", groups),
dockerCommands.get(counter++));
Assert.assertEquals(" hostname=ctr-id", dockerCommands.get(counter++));
Assert.assertEquals(" image=busybox:latest",
dockerCommands.get(counter++));
Assert.assertEquals(
" launch-command=bash,/test_container_work_dir/launch_container.sh",
dockerCommands.get(counter++));
Assert.assertEquals(" name=container_id", dockerCommands.get(counter++));
Assert.assertEquals(" net=host", dockerCommands.get(counter++));
Assert.assertEquals(
" rw-mounts=/test_container_local_dir:/test_container_local_dir,"
+ "/test_filecache_dir:/test_filecache_dir,"
+ "/test_container_work_dir:/test_container_work_dir,"
+ "/test_container_log_dir:/test_container_log_dir,"
+ "/test_user_local_dir:/test_user_local_dir",
dockerCommands.get(counter++));
Assert.assertEquals(" user=" + uidGidPair, dockerCommands.get(counter++));
Assert.assertEquals(" workdir=/test_container_work_dir",
dockerCommands.get(counter++));
}
class MockRuntime extends DockerLinuxContainerRuntime {
private PrivilegedOperationExecutor privilegedOperationExecutor;

View File

@ -36,6 +36,7 @@ public class TestDockerRunCommand {
private static final String CONTAINER_NAME = "foo";
private static final String USER_ID = "user_id";
private static final String IMAGE_NAME = "image_name";
private static final String CLIENT_CONFIG_PATH = "/path/to/client.json";
@Before
public void setUp() throws Exception {
@ -77,4 +78,11 @@ public void testCommandArguments() {
.get("launch-command")));
assertEquals(7, dockerRunCommand.getDockerCommandWithArguments().size());
}
@Test
public void testSetClientConfigDir() {
dockerRunCommand.setClientConfigDir(CLIENT_CONFIG_PATH);
assertEquals(CLIENT_CONFIG_PATH, StringUtils.join(",",
dockerRunCommand.getDockerCommandWithArguments().get("docker-config")));
}
}

View File

@ -380,11 +380,14 @@ For [YARN Service HTTPD example](./yarn-service/Examples.html), container-execut
Connecting to a Secure Docker Repository
----------------------------------------
Until YARN-5428 is complete, the Docker client command will draw its
configuration from the default location, which is $HOME/.docker/config.json on
the NodeManager host. The Docker configuration is where secure repository
credentials are stored, so use of the LCE with secure Docker repos is
discouraged until YARN-5428 is complete.
The Docker client command will draw its configuration from the default location,
which is $HOME/.docker/config.json on the NodeManager host. The Docker
configuration is where secure repository credentials are stored, so use of the
LCE with secure Docker repos is discouraged using this method.
YARN-5428 added support to Distributed Shell for securely supplying the Docker
client configuration. See the Distributed Shell help for usage. Support for
additional frameworks is planned.
As a work-around, you may manually log the Docker daemon on every NodeManager
host into the secure repo using the Docker login command: