Skip to content

Commit eaaaba1

Browse files
HADOOP-16939 fs.s3a.authoritative.path should support multiple FS URIs (#1914)
add unit test, new ITest and then fix the issue: different schema, bucket == skip factored out the underlying logic for unit testing; also moved maybeAddTrailingSlash to S3AUtils (while retaining/forwarnding existing method in S3AFS). tested: london, sole failure is testListingDelete[auth=true](org.apache.hadoop.fs.s3a.ITestS3GuardOutOfBandOperations) filed HADOOP-16853 Change-Id: I4b8d0024469551eda0ec70b4968cba4abed405ed
1 parent 745a6c1 commit eaaaba1

File tree

5 files changed

+203
-8
lines changed

5 files changed

+203
-8
lines changed

hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AFileSystem.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -974,11 +974,7 @@ public String pathToKey(Path path) {
974974
*/
975975
@InterfaceAudience.Private
976976
public String maybeAddTrailingSlash(String key) {
977-
if (!key.isEmpty() && !key.endsWith("/")) {
978-
return key + '/';
979-
} else {
980-
return key;
981-
}
977+
return S3AUtils.maybeAddTrailingSlash(key);
982978
}
983979

984980
/**

hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AUtils.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1726,6 +1726,21 @@ public static String getBucketOption(Configuration conf, String bucket,
17261726
return conf.get(FS_S3A_BUCKET_PREFIX + bucket + '.' + baseKey);
17271727
}
17281728

1729+
/**
1730+
* Turns a path (relative or otherwise) into an S3 key, adding a trailing
1731+
* "/" if the path is not the root <i>and</i> does not already have a "/"
1732+
* at the end.
1733+
*
1734+
* @param key s3 key or ""
1735+
* @return the with a trailing "/", or, if it is the root key, "",
1736+
*/
1737+
public static String maybeAddTrailingSlash(String key) {
1738+
if (!key.isEmpty() && !key.endsWith("/")) {
1739+
return key + '/';
1740+
} else {
1741+
return key;
1742+
}
1743+
}
17291744

17301745
/**
17311746
* Path filter which ignores any file which starts with . or _.

hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3Guard.java

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.util.Objects;
3131
import java.util.Set;
3232
import java.util.concurrent.TimeUnit;
33+
import java.util.function.Function;
3334
import java.util.stream.Collectors;
3435

3536
import javax.annotation.Nullable;
@@ -974,13 +975,43 @@ public static DirListingMetadata listChildrenWithTtl(MetadataStore ms,
974975
}
975976

976977
public static Collection<String> getAuthoritativePaths(S3AFileSystem fs) {
978+
return getAuthoritativePaths(
979+
fs.getUri(),
980+
fs.getConf(),
981+
p -> fs.maybeAddTrailingSlash(fs.qualify(p).toString()));
982+
}
983+
984+
/**
985+
* Get the authoritative paths of a filesystem.
986+
*
987+
* @param uri FS URI
988+
* @param conf configuration
989+
* @param qualifyToDir a qualification operation
990+
* @return list of URIs valid for this FS.
991+
*/
992+
@VisibleForTesting
993+
static Collection<String> getAuthoritativePaths(
994+
final URI uri,
995+
final Configuration conf,
996+
final Function<Path, String> qualifyToDir) {
977997
String[] rawAuthoritativePaths =
978-
fs.getConf().getTrimmedStrings(AUTHORITATIVE_PATH, DEFAULT_AUTHORITATIVE_PATH);
998+
conf.getTrimmedStrings(AUTHORITATIVE_PATH, DEFAULT_AUTHORITATIVE_PATH);
979999
Collection<String> authoritativePaths = new ArrayList<>();
9801000
if (rawAuthoritativePaths.length > 0) {
9811001
for (int i = 0; i < rawAuthoritativePaths.length; i++) {
982-
Path qualified = fs.qualify(new Path(rawAuthoritativePaths[i]));
983-
authoritativePaths.add(fs.maybeAddTrailingSlash(qualified.toString()));
1002+
Path path = new Path(rawAuthoritativePaths[i]);
1003+
URI pathURI = path.toUri();
1004+
if (pathURI.getAuthority() != null &&
1005+
!pathURI.getAuthority().equals(uri.getAuthority())) {
1006+
// skip on auth
1007+
continue;
1008+
}
1009+
if (pathURI.getScheme() != null &&
1010+
!pathURI.getScheme().equals(uri.getScheme())) {
1011+
// skip on auth
1012+
continue;
1013+
}
1014+
authoritativePaths.add(qualifyToDir.apply(path));
9841015
}
9851016
}
9861017
return authoritativePaths;

hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestAuthoritativePath.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,28 @@ public void testSingleAuthPath() throws Exception {
233233
}
234234
}
235235

236+
@Test
237+
public void testAuthPathWithOtherBucket() throws Exception {
238+
Path authPath;
239+
Path nonAuthPath;
240+
S3AFileSystem fs = null;
241+
String landsat = "s3a://landsat-pds/data";
242+
String decoy2 = "/decoy2";
243+
244+
try {
245+
authPath = new Path(testRoot, "testMultiAuthPath-first");
246+
nonAuthPath = new Path(testRoot, "nonAuth-1");
247+
fs = createMultiPathAuthFS(authPath.toString(), landsat, decoy2);
248+
assertTrue("No S3Guard store for partially authoritative FS",
249+
fs.hasMetadataStore());
250+
251+
runTestInsidePath(fs, authPath);
252+
runTestOutsidePath(fs, nonAuthPath);
253+
} finally {
254+
cleanUpFS(fs);
255+
}
256+
}
257+
236258
@Test
237259
public void testMultiAuthPath() throws Exception {
238260
Path authPath;
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.hadoop.fs.s3a.s3guard;
20+
21+
import java.net.URI;
22+
import java.util.Arrays;
23+
import java.util.Collection;
24+
import java.util.List;
25+
import java.util.stream.Collectors;
26+
27+
import org.junit.Before;
28+
import org.junit.Test;
29+
30+
import org.apache.hadoop.conf.Configuration;
31+
import org.apache.hadoop.fs.Path;
32+
import org.apache.hadoop.fs.s3a.S3AUtils;
33+
import org.apache.hadoop.test.AbstractHadoopTestBase;
34+
35+
import static org.apache.hadoop.fs.s3a.Constants.AUTHORITATIVE_PATH;
36+
import static org.assertj.core.api.Assertions.assertThat;
37+
38+
/**
39+
* Unit tests of auth path resolution.
40+
*/
41+
public class TestAuthoritativePath extends AbstractHadoopTestBase {
42+
43+
private final Path root = new Path("/");
44+
45+
private URI fsUri;
46+
47+
private static final String BASE = "s3a://bucket";
48+
49+
@Before
50+
public void setup() throws Exception {
51+
fsUri = new URI(BASE +"/");
52+
}
53+
54+
private Configuration authPathsConf(String... paths) {
55+
Configuration conf = new Configuration(false);
56+
conf.set(AUTHORITATIVE_PATH, String.join(",", paths));
57+
return conf;
58+
}
59+
60+
@Test
61+
public void testResolution() throws Throwable {
62+
assertAuthPaths(l("/one"), "/one/");
63+
}
64+
65+
@Test
66+
public void testResolutionWithFQP() throws Throwable {
67+
assertAuthPaths(l("/one/",
68+
BASE + "/two/"),
69+
"/one/", "/two/");
70+
}
71+
@Test
72+
public void testOtherBucket() throws Throwable {
73+
assertAuthPaths(l("/one/",
74+
"s3a://landsat-pds/",
75+
BASE + "/two/"),
76+
"/one/", "/two/");
77+
}
78+
79+
@Test
80+
public void testOtherScheme() throws Throwable {
81+
assertAuthPaths(l("/one/",
82+
"s3a://landsat-pds/",
83+
"http://bucket/two/"),
84+
"/one/");
85+
}
86+
87+
/**
88+
* Get the auth paths; qualification is through
89+
* Path.makeQualified not the FS near-equivalent.
90+
* @param conf configuration
91+
* @return list of auth paths.
92+
*/
93+
private Collection<String> getAuthoritativePaths(
94+
Configuration conf) {
95+
96+
return S3Guard.getAuthoritativePaths(fsUri, conf,
97+
p -> {
98+
Path q = p.makeQualified(fsUri, root);
99+
assertThat(q.toUri().getAuthority())
100+
.describedAs("Path %s", q)
101+
.isEqualTo(fsUri.getAuthority());
102+
return S3AUtils.maybeAddTrailingSlash(q.toString());
103+
});
104+
}
105+
106+
/**
107+
* take a varargs list and and return as an array.
108+
* @param s source
109+
* @return the values
110+
*/
111+
private String[] l(String...s) {
112+
return s;
113+
}
114+
115+
/**
116+
* Assert that the authoritative paths from a source list
117+
* are that expected.
118+
* @param src source entries to set as auth paths
119+
* @param expected the list of auth paths for a filesystem
120+
*/
121+
private void assertAuthPaths(String[] src, String...expected) {
122+
Configuration conf = authPathsConf(src);
123+
List<String> collect = Arrays.stream(expected)
124+
.map(s -> BASE + s)
125+
.collect(Collectors.toList());
126+
Collection<String> paths = getAuthoritativePaths(conf);
127+
assertThat(paths)
128+
.containsExactlyInAnyOrderElementsOf(collect);
129+
}
130+
131+
}

0 commit comments

Comments
 (0)