Skip to content

Commit 46c4c37

Browse files
Adds validation for instance.volumes property (#5365)
* Move parsing code for volume properties to ConfigurationTypeHelper and add a unit test * Make VolumeManager do an additional check to require the property to be set (could also be done elsewhere in other overall configuration checks) * Added a validator to the PropertyType to verify the format when it is set * Slightly modified some of the exception messages, so that it does not mention the instance.volumes property itself... since the PropertyType validation could apply to other properties of the same type Co-authored-by: Christopher Tubbs <ctubbsii@apache.org>
1 parent cf74fee commit 46c4c37

File tree

7 files changed

+185
-36
lines changed

7 files changed

+185
-36
lines changed

core/src/main/java/org/apache/accumulo/core/conf/ConfigurationTypeHelper.java

+51
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,27 @@
1818
*/
1919
package org.apache.accumulo.core.conf;
2020

21+
import static java.util.Objects.requireNonNull;
2122
import static java.util.concurrent.TimeUnit.DAYS;
2223
import static java.util.concurrent.TimeUnit.HOURS;
2324
import static java.util.concurrent.TimeUnit.MILLISECONDS;
2425
import static java.util.concurrent.TimeUnit.MINUTES;
2526
import static java.util.concurrent.TimeUnit.SECONDS;
2627

28+
import java.net.URI;
29+
import java.net.URISyntaxException;
30+
import java.util.Arrays;
2731
import java.util.Collections;
2832
import java.util.HashMap;
33+
import java.util.LinkedHashSet;
2934
import java.util.Map;
35+
import java.util.Set;
3036
import java.util.concurrent.TimeUnit;
37+
import java.util.function.Predicate;
38+
import java.util.stream.Collectors;
3139

3240
import org.apache.accumulo.core.classloader.ClassLoaderUtil;
41+
import org.apache.hadoop.fs.Path;
3342
import org.slf4j.Logger;
3443
import org.slf4j.LoggerFactory;
3544

@@ -222,4 +231,46 @@ public static int getNumThreads(String threads) {
222231
}
223232
return nThreads;
224233
}
234+
235+
/**
236+
* Get the set of volumes parsed from a volumes property type, and throw exceptions if the volumes
237+
* aren't valid, are null, contains only blanks, or contains duplicates. An empty string is
238+
* allowed (resulting in an empty set of volumes), to handle the case where the property is not
239+
* set by a user (or... is set to the same as the default, which is equivalent to not being set).
240+
* If the property is required to be set, it is the caller's responsibility to verify that the
241+
* parsed set is non-empty.
242+
*
243+
* @throws IllegalArgumentException when the volumes are set to something that cannot be parsed
244+
*/
245+
public static Set<String> getVolumeUris(String volumes) {
246+
if (requireNonNull(volumes).isEmpty()) {
247+
// special case when the property is not set and defaults to an empty string
248+
return Set.of();
249+
}
250+
var blanksRemoved = Arrays.stream(volumes.split(",")).map(String::strip)
251+
.filter(Predicate.not(String::isEmpty)).collect(Collectors.toList());
252+
if (blanksRemoved.isEmpty()) {
253+
throw new IllegalArgumentException("property contains only blank volumes");
254+
}
255+
var deduplicated = blanksRemoved.stream().map(ConfigurationTypeHelper::normalizeVolume)
256+
.collect(Collectors.toCollection(LinkedHashSet::new));
257+
if (deduplicated.size() < blanksRemoved.size()) {
258+
throw new IllegalArgumentException("property contains duplicate volumes");
259+
}
260+
return deduplicated;
261+
}
262+
263+
private static String normalizeVolume(String volume) {
264+
if (!volume.contains(":")) {
265+
throw new IllegalArgumentException("'" + volume + "' is not a fully qualified URI");
266+
}
267+
try {
268+
// pass through URI to unescape hex encoded chars (e.g. convert %2C to "," char)
269+
return new Path(new URI(volume.strip())).toString();
270+
} catch (URISyntaxException e) {
271+
throw new IllegalArgumentException(
272+
"volume contains '" + volume + "' which has a syntax error", e);
273+
}
274+
}
275+
225276
}

core/src/main/java/org/apache/accumulo/core/conf/Property.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ public enum Property {
135135
+ " HDFS. To use the ChangeSecret tool, run the command: `./bin/accumulo"
136136
+ " admin changeSecret`.",
137137
"1.3.5"),
138-
INSTANCE_VOLUMES("instance.volumes", "", PropertyType.STRING,
138+
INSTANCE_VOLUMES("instance.volumes", "", PropertyType.VOLUMES,
139139
"A comma separated list of dfs uris to use. Files will be stored across"
140140
+ " these filesystems. In some situations, the first volume in this list"
141141
+ " may be treated differently, such as being preferred for writing out"

core/src/main/java/org/apache/accumulo/core/conf/PropertyType.java

+27-6
Original file line numberDiff line numberDiff line change
@@ -139,16 +139,19 @@ public enum PropertyType {
139139
+ " interpreted based on the context of the property to which it applies."),
140140

141141
JSON("json", new ValidJson(),
142-
"An arbitrary string that is represents a valid, parsable generic json object."
143-
+ "The validity of the json object in the context of the property usage is not checked by this type."),
142+
"An arbitrary string that is represents a valid, parsable generic json object. The validity "
143+
+ "of the json object in the context of the property usage is not checked by this type."),
144+
144145
BOOLEAN("boolean", in(false, null, "true", "false"),
145146
"Has a value of either 'true' or 'false' (case-insensitive)"),
146147

147148
URI("uri", x -> true, "A valid URI"),
148149

149150
FILENAME_EXT("file name extension", in(true, RFile.EXTENSION),
150151
"One of the currently supported filename extensions for storing table data files. "
151-
+ "Currently, only " + RFile.EXTENSION + " is supported.");
152+
+ "Currently, only " + RFile.EXTENSION + " is supported."),
153+
154+
VOLUMES("volumes", new ValidVolumes(), "See instance.volumes documentation");
152155

153156
private final String shortname;
154157
private final String format;
@@ -215,13 +218,32 @@ private static class ValidJson implements Predicate<String> {
215218
public boolean test(String value) {
216219
try {
217220
if (value.length() > ONE_MILLION) {
218-
log.info("provided json string length {} is greater than limit of {} for parsing",
221+
log.error("provided json string length {} is greater than limit of {} for parsing",
219222
value.length(), ONE_MILLION);
220223
return false;
221224
}
222225
jsonMapper.readTree(value);
223226
return true;
224-
} catch (IOException ex) {
227+
} catch (IOException e) {
228+
log.error("provided json string resulted in an error", e);
229+
return false;
230+
}
231+
}
232+
}
233+
234+
private static class ValidVolumes implements Predicate<String> {
235+
private static final Logger log = LoggerFactory.getLogger(ValidVolumes.class);
236+
237+
@Override
238+
public boolean test(String volumes) {
239+
if (volumes == null) {
240+
return false;
241+
}
242+
try {
243+
ConfigurationTypeHelper.getVolumeUris(volumes);
244+
return true;
245+
} catch (IllegalArgumentException e) {
246+
log.error("provided volume string is not valid", e);
225247
return false;
226248
}
227249
}
@@ -395,5 +417,4 @@ public static IntStream parse(String portRange) {
395417
}
396418

397419
}
398-
399420
}

core/src/main/java/org/apache/accumulo/core/volume/VolumeConfiguration.java

+3-29
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,10 @@
1919
package org.apache.accumulo.core.volume;
2020

2121
import java.io.IOException;
22-
import java.net.URI;
23-
import java.net.URISyntaxException;
24-
import java.util.Arrays;
25-
import java.util.LinkedHashSet;
2622
import java.util.Set;
27-
import java.util.stream.Collectors;
2823

2924
import org.apache.accumulo.core.conf.AccumuloConfiguration;
25+
import org.apache.accumulo.core.conf.ConfigurationTypeHelper;
3026
import org.apache.accumulo.core.conf.Property;
3127
import org.apache.hadoop.conf.Configuration;
3228
import org.apache.hadoop.fs.FileSystem;
@@ -39,34 +35,12 @@ public static FileSystem fileSystemForPath(String path, Configuration conf) thro
3935
}
4036

4137
public static Set<String> getVolumeUris(AccumuloConfiguration conf) {
42-
String volumes = conf.get(Property.INSTANCE_VOLUMES);
38+
var volumes = conf.get(Property.INSTANCE_VOLUMES);
4339
if (volumes == null || volumes.isBlank()) {
4440
throw new IllegalArgumentException(
4541
"Missing required property " + Property.INSTANCE_VOLUMES.getKey());
4642
}
47-
String[] volArray = volumes.split(",");
48-
LinkedHashSet<String> deduplicated =
49-
Arrays.stream(volArray).map(VolumeConfiguration::normalizeVolume)
50-
.collect(Collectors.toCollection(LinkedHashSet::new));
51-
if (deduplicated.size() < volArray.length) {
52-
throw new IllegalArgumentException(
53-
Property.INSTANCE_VOLUMES.getKey() + " contains duplicate volumes (" + volumes + ")");
54-
}
55-
return deduplicated;
56-
}
57-
58-
private static String normalizeVolume(String volume) {
59-
if (volume == null || volume.isBlank() || !volume.contains(":")) {
60-
throw new IllegalArgumentException("Expected fully qualified URI for "
61-
+ Property.INSTANCE_VOLUMES.getKey() + " got " + volume);
62-
}
63-
try {
64-
// pass through URI to unescape hex encoded chars (e.g. convert %2C to "," char)
65-
return new Path(new URI(volume.strip())).toString();
66-
} catch (URISyntaxException e) {
67-
throw new IllegalArgumentException(Property.INSTANCE_VOLUMES.getKey() + " contains '" + volume
68-
+ "' which has a syntax error", e);
69-
}
43+
return ConfigurationTypeHelper.getVolumeUris(volumes);
7044
}
7145

7246
}

core/src/test/java/org/apache/accumulo/core/conf/ConfigurationTypeHelperTest.java

+50
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
import static java.util.concurrent.TimeUnit.SECONDS;
2525
import static org.junit.jupiter.api.Assertions.assertEquals;
2626
import static org.junit.jupiter.api.Assertions.assertThrows;
27+
import static org.junit.jupiter.api.Assertions.assertTrue;
2728

29+
import java.util.Set;
2830
import java.util.function.Function;
2931
import java.util.stream.Stream;
3032

@@ -132,4 +134,52 @@ public void testGetFractionFailureCase2() {
132134
public void testGetFractionFailureCase3() {
133135
assertThrows(IllegalArgumentException.class, () -> ConfigurationTypeHelper.getFraction(".%"));
134136
}
137+
138+
@Test
139+
public void testGetVolumeUris() {
140+
// test property not set
141+
assertEquals(Set.of(), ConfigurationTypeHelper.getVolumeUris(""));
142+
143+
// test blank cases
144+
assertThrows(NullPointerException.class, () -> ConfigurationTypeHelper.getVolumeUris(null));
145+
for (String s : Set.of(" ", ",", ",,,", " ,,,", ",,, ", ", ,,")) {
146+
var e = assertThrows(IllegalArgumentException.class,
147+
() -> ConfigurationTypeHelper.getVolumeUris(s));
148+
assertEquals("property contains only blank volumes", e.getMessage());
149+
}
150+
151+
// test 1 volume
152+
for (String s : Set.of("hdfs:/volA", ",hdfs:/volA", "hdfs:/volA,")) {
153+
var uris = ConfigurationTypeHelper.getVolumeUris(s);
154+
assertEquals(1, uris.size());
155+
assertTrue(uris.contains("hdfs:/volA"));
156+
}
157+
158+
// test more than 1 volume
159+
for (String s : Set.of("hdfs:/volA,file:/volB", ",hdfs:/volA,file:/volB",
160+
"hdfs:/volA,,file:/volB", "hdfs:/volA,file:/volB, ,")) {
161+
var uris = ConfigurationTypeHelper.getVolumeUris(s);
162+
assertEquals(2, uris.size());
163+
assertTrue(uris.contains("hdfs:/volA"));
164+
assertTrue(uris.contains("file:/volB"));
165+
}
166+
167+
// test invalid URI
168+
for (String s : Set.of("hdfs:/volA,hdfs:/volB,volA", ",volA,hdfs:/volA,hdfs:/volB",
169+
"hdfs:/volA,,volA,hdfs:/volB", "hdfs:/volA,volA,hdfs:/volB, ,")) {
170+
var iae = assertThrows(IllegalArgumentException.class,
171+
() -> ConfigurationTypeHelper.getVolumeUris(s));
172+
assertEquals("'volA' is not a fully qualified URI", iae.getMessage());
173+
}
174+
175+
// test duplicates
176+
var iae = assertThrows(IllegalArgumentException.class,
177+
() -> ConfigurationTypeHelper.getVolumeUris("hdfs:/volA,hdfs:/volB,hdfs:/volA"));
178+
assertEquals("property contains duplicate volumes", iae.getMessage());
179+
180+
// test syntax error in URI
181+
iae = assertThrows(IllegalArgumentException.class,
182+
() -> ConfigurationTypeHelper.getVolumeUris("hdfs:/volA,hdfs :/::/volB"));
183+
assertEquals("volume contains 'hdfs :/::/volB' which has a syntax error", iae.getMessage());
184+
}
135185
}

core/src/test/java/org/apache/accumulo/core/conf/PropertyTypeTest.java

+11
Original file line numberDiff line numberDiff line change
@@ -225,4 +225,15 @@ public void testTypeFILENAME_EXT() {
225225
invalid(null, "RF", "map", "", "MAP", "rF", "Rf", " rf ");
226226
}
227227

228+
@Test
229+
public void testTypeVOLUMES() {
230+
// more comprehensive parsing tests are in ConfigurationTypeHelperTest.testGetVolumeUris()
231+
valid("", "hdfs:/volA", ",hdfs:/volA", "hdfs:/volA,", "hdfs:/volA,file:/volB",
232+
",hdfs:/volA,file:/volB", "hdfs:/volA,,file:/volB", "hdfs:/volA,file:/volB, ,");
233+
invalid(null, " ", ",", ",,,", " ,,,", ",,, ", ", ,,", "hdfs:/volA,hdfs:/volB,volA",
234+
",volA,hdfs:/volA,hdfs:/volB", "hdfs:/volA,,volA,hdfs:/volB",
235+
"hdfs:/volA,volA,hdfs:/volB, ,", "hdfs:/volA,hdfs:/volB,hdfs:/volA",
236+
"hdfs:/volA,hdfs :/::/volB");
237+
}
238+
228239
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.accumulo.core.volume;
20+
21+
import static org.junit.jupiter.api.Assertions.assertNull;
22+
import static org.junit.jupiter.api.Assertions.assertThrows;
23+
24+
import org.apache.accumulo.core.conf.ConfigurationCopy;
25+
import org.apache.accumulo.core.conf.Property;
26+
import org.junit.jupiter.api.Test;
27+
28+
public class VolumeConfigurationTest {
29+
@Test
30+
public void testEmptyVolumes() {
31+
ConfigurationCopy config = new ConfigurationCopy();
32+
assertNull(config.get(Property.INSTANCE_VOLUMES.getKey()));
33+
assertThrows(IllegalArgumentException.class, () -> VolumeConfiguration.getVolumeUris(config));
34+
35+
config.set(Property.INSTANCE_VOLUMES.getKey(), "");
36+
assertThrows(IllegalArgumentException.class, () -> VolumeConfiguration.getVolumeUris(config));
37+
config.set(Property.INSTANCE_VOLUMES.getKey(), " ");
38+
assertThrows(IllegalArgumentException.class, () -> VolumeConfiguration.getVolumeUris(config));
39+
config.set(Property.INSTANCE_VOLUMES.getKey(), ",");
40+
assertThrows(IllegalArgumentException.class, () -> VolumeConfiguration.getVolumeUris(config));
41+
}
42+
}

0 commit comments

Comments
 (0)