YARN-8429. Improve diagnostic message when artifact is not set properly.

Contributed by Gour Saha
This commit is contained in:
Eric Yang 2018-07-26 20:02:13 -04:00
parent 77721f39e2
commit 8d3c068e59
9 changed files with 140 additions and 56 deletions

View File

@ -50,6 +50,10 @@ public interface RestApiErrorMessages {
"Artifact id (like docker image name) is either empty or not provided"; "Artifact id (like docker image name) is either empty or not provided";
String ERROR_ARTIFACT_ID_FOR_COMP_INVALID = String ERROR_ARTIFACT_ID_FOR_COMP_INVALID =
ERROR_ARTIFACT_ID_INVALID + ERROR_SUFFIX_FOR_COMPONENT; ERROR_ARTIFACT_ID_INVALID + ERROR_SUFFIX_FOR_COMPONENT;
String ERROR_ARTIFACT_PATH_FOR_COMP_INVALID = "For component %s with %s "
+ "artifact, path does not exist: %s";
String ERROR_CONFIGFILE_DEST_FILE_FOR_COMP_NOT_ABSOLUTE = "For component %s "
+ "with %s artifact, dest_file must be a relative path: %s";
String ERROR_RESOURCE_INVALID = "Resource is not provided"; String ERROR_RESOURCE_INVALID = "Resource is not provided";
String ERROR_RESOURCE_FOR_COMP_INVALID = String ERROR_RESOURCE_FOR_COMP_INVALID =
@ -89,7 +93,7 @@ public interface RestApiErrorMessages {
String ERROR_ABSENT_NUM_OF_INSTANCE = String ERROR_ABSENT_NUM_OF_INSTANCE =
"Num of instances should appear either globally or per component"; "Num of instances should appear either globally or per component";
String ERROR_ABSENT_LAUNCH_COMMAND = String ERROR_ABSENT_LAUNCH_COMMAND =
"Launch_command is required when type is not DOCKER"; "launch_command is required when type is not DOCKER";
String ERROR_QUICKLINKS_FOR_COMP_INVALID = "Quicklinks specified at" String ERROR_QUICKLINKS_FOR_COMP_INVALID = "Quicklinks specified at"
+ " component level, needs corresponding values set at service level"; + " component level, needs corresponding values set at service level";

View File

@ -68,18 +68,18 @@ public static final Set<String> createApplicationTags(String appName,
* Validate the artifact. * Validate the artifact.
* @param artifact * @param artifact
*/ */
public abstract void validateArtifact(Artifact artifact, FileSystem public abstract void validateArtifact(Artifact artifact, String compName,
fileSystem) throws IOException; FileSystem fileSystem) throws IOException;
protected abstract void validateConfigFile(ConfigFile configFile, FileSystem protected abstract void validateConfigFile(ConfigFile configFile,
fileSystem) throws IOException; String compName, FileSystem fileSystem) throws IOException;
/** /**
* Validate the config files. * Validate the config files.
* @param configFiles config file list * @param configFiles config file list
* @param fs file system * @param fs file system
*/ */
public void validateConfigFiles(List<ConfigFile> configFiles, public void validateConfigFiles(List<ConfigFile> configFiles, String compName,
FileSystem fs) throws IOException { FileSystem fs) throws IOException {
Set<String> destFileSet = new HashSet<>(); Set<String> destFileSet = new HashSet<>();
@ -128,7 +128,7 @@ public void validateConfigFiles(List<ConfigFile> configFiles,
} }
if (StringUtils.isEmpty(file.getDestFile())) { if (StringUtils.isEmpty(file.getDestFile())) {
throw new IllegalArgumentException("Dest_file is empty."); throw new IllegalArgumentException("dest_file is empty.");
} }
if (destFileSet.contains(file.getDestFile())) { if (destFileSet.contains(file.getDestFile())) {
@ -144,7 +144,7 @@ public void validateConfigFiles(List<ConfigFile> configFiles,
} }
// provider-specific validation // provider-specific validation
validateConfigFile(file, fs); validateConfigFile(file, compName, fs);
} }
} }
} }

View File

@ -17,30 +17,36 @@
*/ */
package org.apache.hadoop.yarn.service.provider.defaultImpl; package org.apache.hadoop.yarn.service.provider.defaultImpl;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.yarn.service.provider.AbstractClientProvider;
import org.apache.hadoop.yarn.service.api.records.Artifact;
import org.apache.hadoop.yarn.service.api.records.ConfigFile;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Paths; import java.nio.file.Paths;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.yarn.service.api.records.Artifact;
import org.apache.hadoop.yarn.service.api.records.ConfigFile;
import org.apache.hadoop.yarn.service.exceptions.RestApiErrorMessages;
import org.apache.hadoop.yarn.service.provider.AbstractClientProvider;
import com.google.common.annotations.VisibleForTesting;
public class DefaultClientProvider extends AbstractClientProvider { public class DefaultClientProvider extends AbstractClientProvider {
public DefaultClientProvider() { public DefaultClientProvider() {
} }
@Override @Override
public void validateArtifact(Artifact artifact, FileSystem fileSystem) { public void validateArtifact(Artifact artifact, String compName,
FileSystem fileSystem) {
} }
@Override @Override
protected void validateConfigFile(ConfigFile configFile, FileSystem @VisibleForTesting
fileSystem) throws IOException { public void validateConfigFile(ConfigFile configFile, String compName,
FileSystem fileSystem) throws IOException {
// validate dest_file is not absolute // validate dest_file is not absolute
if (Paths.get(configFile.getDestFile()).isAbsolute()) { if (Paths.get(configFile.getDestFile()).isAbsolute()) {
throw new IllegalArgumentException( throw new IllegalArgumentException(String.format(
"Dest_file must not be absolute path: " + configFile.getDestFile()); RestApiErrorMessages.ERROR_CONFIGFILE_DEST_FILE_FOR_COMP_NOT_ABSOLUTE,
compName, "no", configFile.getDestFile()));
} }
} }
} }

View File

@ -35,19 +35,20 @@ public DockerClientProvider() {
} }
@Override @Override
public void validateArtifact(Artifact artifact, FileSystem fileSystem) { public void validateArtifact(Artifact artifact, String compName,
FileSystem fileSystem) {
if (artifact == null) { if (artifact == null) {
throw new IllegalArgumentException( throw new IllegalArgumentException(String.format(
RestApiErrorMessages.ERROR_ARTIFACT_INVALID); RestApiErrorMessages.ERROR_ARTIFACT_FOR_COMP_INVALID, compName));
} }
if (StringUtils.isEmpty(artifact.getId())) { if (StringUtils.isEmpty(artifact.getId())) {
throw new IllegalArgumentException( throw new IllegalArgumentException(String.format(
RestApiErrorMessages.ERROR_ARTIFACT_ID_INVALID); RestApiErrorMessages.ERROR_ARTIFACT_ID_FOR_COMP_INVALID, compName));
} }
} }
@Override @Override
protected void validateConfigFile(ConfigFile configFile, FileSystem protected void validateConfigFile(ConfigFile configFile, String compName,
fileSystem) throws IOException { FileSystem fileSystem) throws IOException {
} }
} }

View File

@ -36,30 +36,33 @@ public TarballClientProvider() {
} }
@Override @Override
public void validateArtifact(Artifact artifact, FileSystem fs) public void validateArtifact(Artifact artifact, String compName,
throws IOException { FileSystem fs) throws IOException {
if (artifact == null) { if (artifact == null) {
throw new IllegalArgumentException( throw new IllegalArgumentException(String.format(
RestApiErrorMessages.ERROR_ARTIFACT_INVALID); RestApiErrorMessages.ERROR_ARTIFACT_FOR_COMP_INVALID, compName));
} }
if (StringUtils.isEmpty(artifact.getId())) { if (StringUtils.isEmpty(artifact.getId())) {
throw new IllegalArgumentException( throw new IllegalArgumentException(String.format(
RestApiErrorMessages.ERROR_ARTIFACT_ID_INVALID); RestApiErrorMessages.ERROR_ARTIFACT_ID_FOR_COMP_INVALID, compName));
} }
Path p = new Path(artifact.getId()); Path p = new Path(artifact.getId());
if (!fs.exists(p)) { if (!fs.exists(p)) {
throw new IllegalArgumentException( "Artifact tarball does not exist " throw new IllegalArgumentException(String.format(
+ artifact.getId()); RestApiErrorMessages.ERROR_ARTIFACT_PATH_FOR_COMP_INVALID, compName,
Artifact.TypeEnum.TARBALL.name(), artifact.getId()));
} }
} }
@Override @Override
protected void validateConfigFile(ConfigFile configFile, FileSystem protected void validateConfigFile(ConfigFile configFile, String compName,
fileSystem) throws IOException { FileSystem fileSystem) throws IOException {
// validate dest_file is not absolute // validate dest_file is not absolute
if (Paths.get(configFile.getDestFile()).isAbsolute()) { if (Paths.get(configFile.getDestFile()).isAbsolute()) {
throw new IllegalArgumentException( throw new IllegalArgumentException(String.format(
"Dest_file must not be absolute path: " + configFile.getDestFile()); RestApiErrorMessages.ERROR_CONFIGFILE_DEST_FILE_FOR_COMP_NOT_ABSOLUTE,
compName, Artifact.TypeEnum.TARBALL.name(),
configFile.getDestFile()));
} }
} }
} }

View File

@ -282,7 +282,7 @@ private static void validateComponent(Component comp, FileSystem fs,
AbstractClientProvider compClientProvider = ProviderFactory AbstractClientProvider compClientProvider = ProviderFactory
.getClientProvider(comp.getArtifact()); .getClientProvider(comp.getArtifact());
compClientProvider.validateArtifact(comp.getArtifact(), fs); compClientProvider.validateArtifact(comp.getArtifact(), comp.getName(), fs);
if (comp.getLaunchCommand() == null && (comp.getArtifact() == null || comp if (comp.getLaunchCommand() == null && (comp.getArtifact() == null || comp
.getArtifact().getType() != Artifact.TypeEnum.DOCKER)) { .getArtifact().getType() != Artifact.TypeEnum.DOCKER)) {
@ -299,7 +299,7 @@ private static void validateComponent(Component comp, FileSystem fs,
+ ": " + comp.getNumberOfContainers(), comp.getName())); + ": " + comp.getNumberOfContainers(), comp.getName()));
} }
compClientProvider.validateConfigFiles(comp.getConfiguration() compClientProvider.validateConfigFiles(comp.getConfiguration()
.getFiles(), fs); .getFiles(), comp.getName(), fs);
MonitorUtils.getProbe(comp.getReadinessCheck()); MonitorUtils.getProbe(comp.getReadinessCheck());
} }

View File

@ -227,14 +227,16 @@ public void testArtifacts() throws IOException {
// no artifact id fails with default type // no artifact id fails with default type
Artifact artifact = new Artifact(); Artifact artifact = new Artifact();
app.setArtifact(artifact); app.setArtifact(artifact);
Component comp = ServiceTestUtils.createComponent("comp1"); String compName = "comp1";
Component comp = ServiceTestUtils.createComponent(compName);
app.setComponents(Collections.singletonList(comp)); app.setComponents(Collections.singletonList(comp));
try { try {
ServiceApiUtil.validateAndResolveService(app, sfs, CONF_DNS_ENABLED); ServiceApiUtil.validateAndResolveService(app, sfs, CONF_DNS_ENABLED);
Assert.fail(EXCEPTION_PREFIX + "service with no artifact id"); Assert.fail(EXCEPTION_PREFIX + "service with no artifact id");
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
assertEquals(ERROR_ARTIFACT_ID_INVALID, e.getMessage()); assertEquals(String.format(ERROR_ARTIFACT_ID_FOR_COMP_INVALID, compName),
e.getMessage());
} }
// no artifact id fails with SERVICE type // no artifact id fails with SERVICE type
@ -252,7 +254,8 @@ public void testArtifacts() throws IOException {
ServiceApiUtil.validateAndResolveService(app, sfs, CONF_DNS_ENABLED); ServiceApiUtil.validateAndResolveService(app, sfs, CONF_DNS_ENABLED);
Assert.fail(EXCEPTION_PREFIX + "service with no artifact id"); Assert.fail(EXCEPTION_PREFIX + "service with no artifact id");
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
assertEquals(ERROR_ARTIFACT_ID_INVALID, e.getMessage()); assertEquals(String.format(ERROR_ARTIFACT_ID_FOR_COMP_INVALID, compName),
e.getMessage());
} }
// everything valid here // everything valid here

View File

@ -45,12 +45,12 @@ public class TestAbstractClientProvider {
private static class ClientProvider extends AbstractClientProvider { private static class ClientProvider extends AbstractClientProvider {
@Override @Override
public void validateArtifact(Artifact artifact, FileSystem fileSystem) public void validateArtifact(Artifact artifact, String compName,
throws IOException { FileSystem fileSystem) throws IOException {
} }
@Override @Override
protected void validateConfigFile(ConfigFile configFile, protected void validateConfigFile(ConfigFile configFile, String compName,
FileSystem fileSystem) throws IOException { FileSystem fileSystem) throws IOException {
} }
} }
@ -62,33 +62,34 @@ public void testConfigFiles() throws IOException {
FileStatus mockFileStatus = mock(FileStatus.class); FileStatus mockFileStatus = mock(FileStatus.class);
when(mockFs.exists(anyObject())).thenReturn(true); when(mockFs.exists(anyObject())).thenReturn(true);
String compName = "sleeper";
ConfigFile configFile = new ConfigFile(); ConfigFile configFile = new ConfigFile();
List<ConfigFile> configFiles = new ArrayList<>(); List<ConfigFile> configFiles = new ArrayList<>();
configFiles.add(configFile); configFiles.add(configFile);
try { try {
clientProvider.validateConfigFiles(configFiles, mockFs); clientProvider.validateConfigFiles(configFiles, compName, mockFs);
Assert.fail(EXCEPTION_PREFIX + "null file type"); Assert.fail(EXCEPTION_PREFIX + "null file type");
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
} }
configFile.setType(ConfigFile.TypeEnum.TEMPLATE); configFile.setType(ConfigFile.TypeEnum.TEMPLATE);
try { try {
clientProvider.validateConfigFiles(configFiles, mockFs); clientProvider.validateConfigFiles(configFiles, compName, mockFs);
Assert.fail(EXCEPTION_PREFIX + "empty src_file for type template"); Assert.fail(EXCEPTION_PREFIX + "empty src_file for type template");
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
} }
configFile.setSrcFile("srcfile"); configFile.setSrcFile("srcfile");
try { try {
clientProvider.validateConfigFiles(configFiles, mockFs); clientProvider.validateConfigFiles(configFiles, compName, mockFs);
Assert.fail(EXCEPTION_PREFIX + "empty dest file"); Assert.fail(EXCEPTION_PREFIX + "empty dest file");
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
} }
configFile.setDestFile("destfile"); configFile.setDestFile("destfile");
try { try {
clientProvider.validateConfigFiles(configFiles, mockFs); clientProvider.validateConfigFiles(configFiles, compName, mockFs);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Assert.fail(NO_EXCEPTION_PREFIX + e.getMessage()); Assert.fail(NO_EXCEPTION_PREFIX + e.getMessage());
} }
@ -99,21 +100,21 @@ public void testConfigFiles() throws IOException {
configFile.setDestFile("path/destfile2"); configFile.setDestFile("path/destfile2");
configFiles.add(configFile); configFiles.add(configFile);
try { try {
clientProvider.validateConfigFiles(configFiles, mockFs); clientProvider.validateConfigFiles(configFiles, compName, mockFs);
Assert.fail(EXCEPTION_PREFIX + "dest file with multiple path elements"); Assert.fail(EXCEPTION_PREFIX + "dest file with multiple path elements");
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
} }
configFile.setDestFile("/path/destfile2"); configFile.setDestFile("/path/destfile2");
try { try {
clientProvider.validateConfigFiles(configFiles, mockFs); clientProvider.validateConfigFiles(configFiles, compName, mockFs);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Assert.fail(NO_EXCEPTION_PREFIX + e.getMessage()); Assert.fail(NO_EXCEPTION_PREFIX + e.getMessage());
} }
configFile.setDestFile("destfile"); configFile.setDestFile("destfile");
try { try {
clientProvider.validateConfigFiles(configFiles, mockFs); clientProvider.validateConfigFiles(configFiles, compName, mockFs);
Assert.fail(EXCEPTION_PREFIX + "duplicate dest file"); Assert.fail(EXCEPTION_PREFIX + "duplicate dest file");
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
} }
@ -125,14 +126,14 @@ public void testConfigFiles() throws IOException {
configFile.setDestFile("path/destfile3"); configFile.setDestFile("path/destfile3");
configFiles.add(configFile); configFiles.add(configFile);
try { try {
clientProvider.validateConfigFiles(configFiles, mockFs); clientProvider.validateConfigFiles(configFiles, compName, mockFs);
Assert.fail(EXCEPTION_PREFIX + "dest file with multiple path elements"); Assert.fail(EXCEPTION_PREFIX + "dest file with multiple path elements");
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
} }
configFile.setDestFile("/path/destfile3"); configFile.setDestFile("/path/destfile3");
try { try {
clientProvider.validateConfigFiles(configFiles, mockFs); clientProvider.validateConfigFiles(configFiles, compName, mockFs);
Assert.fail(EXCEPTION_PREFIX + "src file should be specified"); Assert.fail(EXCEPTION_PREFIX + "src file should be specified");
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
} }
@ -140,7 +141,7 @@ public void testConfigFiles() throws IOException {
//should succeed //should succeed
configFile.setSrcFile("srcFile"); configFile.setSrcFile("srcFile");
configFile.setDestFile("destfile3"); configFile.setDestFile("destfile3");
clientProvider.validateConfigFiles(configFiles, mockFs); clientProvider.validateConfigFiles(configFiles, compName, mockFs);
when(mockFileStatus.isDirectory()).thenReturn(true); when(mockFileStatus.isDirectory()).thenReturn(true);
when(mockFs.getFileStatus(new Path("srcFile"))) when(mockFs.getFileStatus(new Path("srcFile")))
@ -154,7 +155,7 @@ public void testConfigFiles() throws IOException {
configFiles.add(configFile); configFiles.add(configFile);
try { try {
clientProvider.validateConfigFiles(configFiles, mockFs); clientProvider.validateConfigFiles(configFiles, compName, mockFs);
Assert.fail(EXCEPTION_PREFIX + "src file is a directory"); Assert.fail(EXCEPTION_PREFIX + "src file is a directory");
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
} }

View File

@ -0,0 +1,66 @@
/*
* 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.service.providers;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.IOException;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.yarn.service.api.records.ConfigFile;
import org.apache.hadoop.yarn.service.exceptions.RestApiErrorMessages;
import org.apache.hadoop.yarn.service.provider.defaultImpl.DefaultClientProvider;
import org.junit.Assert;
import org.junit.Test;
public class TestDefaultClientProvider {
private static final String EXCEPTION_PREFIX = "Should have thrown "
+ "exception: ";
private static final String NO_EXCEPTION_PREFIX = "Should not have thrown "
+ "exception: ";
@Test
public void testConfigFile() throws IOException {
DefaultClientProvider defaultClientProvider = new DefaultClientProvider();
FileSystem mockFs = mock(FileSystem.class);
when(mockFs.exists(anyObject())).thenReturn(true);
String compName = "sleeper";
ConfigFile configFile = new ConfigFile();
configFile.setDestFile("/var/tmp/a.txt");
try {
defaultClientProvider.validateConfigFile(configFile, compName, mockFs);
Assert.fail(EXCEPTION_PREFIX + " dest_file must be relative");
} catch (IllegalArgumentException e) {
String actualMsg = String.format(
RestApiErrorMessages.ERROR_CONFIGFILE_DEST_FILE_FOR_COMP_NOT_ABSOLUTE,
compName, "no", configFile.getDestFile());
Assert.assertEquals(actualMsg, e.getLocalizedMessage());
}
configFile.setDestFile("../a.txt");
try {
defaultClientProvider.validateConfigFile(configFile, compName, mockFs);
} catch (IllegalArgumentException e) {
Assert.fail(NO_EXCEPTION_PREFIX + e.getLocalizedMessage());
}
}
}