Skip to content

Commit dcd7d49

Browse files
committed
generalize the classes such that no external storage system is strictly specified.
1 parent c4b93ee commit dcd7d49

4 files changed

Lines changed: 111 additions & 41 deletions

File tree

sdks/java/extensions/kafka-factories/src/main/java/org/apache/beam/sdk/extensions/kafka/factories/FileAwareFactoryFn.java

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@
4545
public abstract class FileAwareFactoryFn<T>
4646
implements SerializableFunction<Map<String, Object>, T> {
4747

48-
public static final String GCS_PATH_PREFIX = "gs://";
48+
public static String externalBucketPrefix = "";
4949
public static final String SECRET_VALUE_PREFIX = "secretValue:";
5050
public static final String DIRECTORY_PREFIX = "/tmp";
5151
private static final Pattern PATH_PATTERN =
52-
Pattern.compile("(gs://[^\"]+)|(secretValue:[^\"]+)|(secretFile:[^\"]+)");
52+
Pattern.compile("([a-zA-Z0-9]+://[^\"]+)|(secretValue:[^\"]+)|(secretFile:[^\"]+)");
5353

5454
private static final Map<String, byte[]> secretCache = new ConcurrentHashMap<>();
5555

@@ -86,18 +86,18 @@ public T apply(Map<String, Object> config) {
8686
StringBuffer sb = new StringBuffer();
8787

8888
while (matcher.find()) {
89-
String gcsPath = matcher.group(1);
89+
String externalPath = matcher.group(1);
9090
String secretValue = matcher.group(2);
9191
String secretFile = matcher.group(3);
9292

93-
if (gcsPath != null) {
93+
if (externalPath != null) {
9494
try {
95-
String tmpPath = replacePathWithLocal(gcsPath);
96-
String localPath = downloadGcsFile(gcsPath, tmpPath);
95+
String tmpPath = replacePathWithLocal(externalPath);
96+
String localPath = downloadExternalFile(externalPath, tmpPath);
9797
matcher.appendReplacement(sb, Matcher.quoteReplacement(localPath));
98-
LOG.info("Downloaded {} to {}", gcsPath, localPath);
98+
LOG.info("Downloaded {} to {}", externalPath, localPath);
9999
} catch (IOException io) {
100-
throw new IOException("Failed to download file : " + gcsPath, io);
100+
throw new IOException("Failed to download file : " + externalPath, io);
101101
}
102102
} else if (secretValue != null) {
103103
try {
@@ -131,16 +131,16 @@ public T apply(Map<String, Object> config) {
131131
}
132132

133133
/**
134-
* A function to download files from their specified gcs path and copy them to the provided local
135-
* filepath. The local filepath is provided by the replacePathWithLocal.
134+
* A function to download files from their specified external storage path and copy them to the
135+
* provided local filepath. The local filepath is provided by the replacePathWithLocal.
136136
*
137-
* @param gcsFilePath
137+
* @param externalFilePath
138138
* @param outputFileString
139139
* @return
140140
* @throws IOException
141141
*/
142-
protected static synchronized String downloadGcsFile(String gcsFilePath, String outputFileString)
143-
throws IOException {
142+
protected static synchronized String downloadExternalFile(
143+
String externalFilePath, String outputFileString) throws IOException {
144144
// create the file only if it doesn't exist
145145
if (new File(outputFileString).exists()) {
146146
return outputFileString;
@@ -150,14 +150,15 @@ protected static synchronized String downloadGcsFile(String gcsFilePath, String
150150
if (parentDir != null) {
151151
Files.createDirectories(parentDir);
152152
}
153-
LOG.info("Staging GCS file [{}] to [{}]", gcsFilePath, outputFileString);
153+
LOG.info("Staging external file [{}] to [{}]", externalFilePath, outputFileString);
154154
Set<StandardOpenOption> options = new HashSet<>(2);
155155
options.add(StandardOpenOption.CREATE);
156156
options.add(StandardOpenOption.WRITE);
157157

158-
// Copy the GCS file into a local file and will throw an I/O exception in case file not found.
158+
// Copy the external file into a local file and will throw an I/O exception in case file not
159+
// found.
159160
try (ReadableByteChannel readerChannel =
160-
FileSystems.open(FileSystems.matchSingleFileSpec(gcsFilePath).resourceId())) {
161+
FileSystems.open(FileSystems.matchSingleFileSpec(externalFilePath).resourceId())) {
161162
try (FileChannel writeChannel = FileChannel.open(outputFilePath, options)) {
162163
writeChannel.transferFrom(readerChannel, 0, Long.MAX_VALUE);
163164
}
@@ -170,16 +171,25 @@ protected byte[] getSecretWithCache(String secretId) {
170171
}
171172

172173
/**
173-
* A helper method to create a new string with the gcs paths replaced with their local path and
174-
* subdirectory based on the factory type in the /tmp directory. For example, the kerberos factory
175-
* type will replace the file paths with /tmp/kerberos/file.path
174+
* A helper method to create a new string with the external paths replaced with their local path
175+
* and subdirectory based on the factory type in the /tmp directory. For example, the kerberos
176+
* factory type will replace the file paths with /tmp/kerberos/file.path
176177
*
177-
* @param gcsPath
178-
* @return a string with all instances of GCS paths converted to the local paths where the files
179-
* sit.
178+
* @param externalPath
179+
* @return a string with all instances of external paths converted to the local paths where the
180+
* files sit.
180181
*/
181-
private String replacePathWithLocal(String gcsPath) throws IOException {
182-
return DIRECTORY_PREFIX + "/" + factoryType + "/" + gcsPath.substring(GCS_PATH_PREFIX.length());
182+
private String replacePathWithLocal(String externalPath) throws IOException {
183+
String externalBucketPrefixIdentifier = "://";
184+
int externalBucketPrefixIndex = externalPath.lastIndexOf(externalBucketPrefixIdentifier);
185+
if (externalBucketPrefixIndex == -1) {
186+
// if we don't find a known bucket prefix then we will error early.
187+
throw new RuntimeException(
188+
"The provided external bucket could not be matched to a known source.");
189+
}
190+
191+
int prefixLength = externalBucketPrefixIndex + externalBucketPrefixIdentifier.length();
192+
return DIRECTORY_PREFIX + "/" + factoryType + "/" + externalPath.substring(prefixLength);
183193
}
184194

185195
/**

sdks/java/extensions/kafka-factories/src/main/java/org/apache/beam/sdk/extensions/kafka/factories/KerberosConsumerFactoryFn.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838

3939
public class KerberosConsumerFactoryFn extends FileAwareFactoryFn<Consumer<byte[], byte[]>> {
4040
private static final String LOCAL_FACTORY_TYPE = "kerberos";
41-
private String krb5ConfigGcsPath = "";
41+
private String krb5ConfigPath = "";
4242
private static volatile String localKrb5ConfPath = "";
4343

4444
private static final Object lock = new Object();
@@ -51,16 +51,16 @@ public class KerberosConsumerFactoryFn extends FileAwareFactoryFn<Consumer<byte[
5151

5252
private static final Logger LOG = LoggerFactory.getLogger(KerberosConsumerFactoryFn.class);
5353

54-
public KerberosConsumerFactoryFn(String krb5ConfigGcsPath) {
54+
public KerberosConsumerFactoryFn(String krb5ConfigPath) {
5555
super("kerberos");
56-
this.krb5ConfigGcsPath = krb5ConfigGcsPath;
56+
this.krb5ConfigPath = krb5ConfigPath;
5757
}
5858

5959
@Override
6060
protected Consumer<byte[], byte[]> createObject(Map<String, Object> config) {
6161
// This will be called after the config map processing has occurred. Therefore, we know that the
6262
// property will have had it's value replaced with a local directory.
63-
// We don't need to worry about the GCS prefix in this case.
63+
// We don't need to worry about the external bucket prefix in this case.
6464
try {
6565
String jaasConfig = (String) config.get(JAAS_CONFIG_PROPERTY);
6666
String localKeytabPath = "";
@@ -90,10 +90,10 @@ protected void downloadAndProcessExtraFiles() throws IOException {
9090
synchronized (lock) {
9191
// we only want a new krb5 file if there is not already one present.
9292
if (localKrb5ConfPath.isEmpty()) {
93-
if (this.krb5ConfigGcsPath != null && !this.krb5ConfigGcsPath.isEmpty()) {
93+
if (this.krb5ConfigPath != null && !this.krb5ConfigPath.isEmpty()) {
9494
String localPath =
9595
super.getBaseDirectory() + "/" + LOCAL_FACTORY_TYPE + "/" + "krb5.conf";
96-
localKrb5ConfPath = downloadGcsFile(this.krb5ConfigGcsPath, localPath);
96+
localKrb5ConfPath = downloadExternalFile(this.krb5ConfigPath, localPath);
9797

9898
System.setProperty("java.security.krb5.conf", localKrb5ConfPath);
9999
Configuration.getConfiguration().refresh();

sdks/java/extensions/kafka-factories/src/test/java/org/apache/beam/sdk/extensions/kafka/factories/FileAwareFactoryFnTest.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public void setup() throws IOException {
7272
}
7373

7474
@Test
75-
public void testHappyPathReplacesGcsPath() {
75+
public void testHappyPathReplacesExternalPath() {
7676
// Arrange
7777
String gcsPath = "gs://test-bucket/config-file.json";
7878
String expectedLocalPath =
@@ -118,7 +118,6 @@ public void testApplyFailurePathThrowsRuntimeExceptionOnDownloadFailure() {
118118
mockedFileSystems
119119
.when(() -> FileSystems.matchSingleFileSpec(gcsPath))
120120
.thenThrow(new IOException("GCS file not found"));
121-
122121
// Act & Assert
123122
RuntimeException exception =
124123
Assert.assertThrows(RuntimeException.class, () -> factory.apply(config));
@@ -129,7 +128,7 @@ public void testApplyFailurePathThrowsRuntimeExceptionOnDownloadFailure() {
129128
}
130129

131130
@Test
132-
public void testApplyHappyPathIgnoresNonGcsValues() {
131+
public void testApplyHappyPathIgnoresNonExternalValues() {
133132
// Arrange
134133
Map<String, Object> config = new HashMap<>();
135134
config.put("some.string", "/local/path/file.txt");
@@ -144,7 +143,7 @@ public void testApplyHappyPathIgnoresNonGcsValues() {
144143
}
145144

146145
@Test
147-
public void testApplyEdgeCaseMultipleGcsPathsInSingleValue() {
146+
public void testApplyEdgeCaseMultipleExternalPathsInSingleValue() {
148147
// Arrange
149148
String gcsPath1 = "gs://bucket/keytab.keytab";
150149
String gcsPath2 = "gs://bucket/trust.jks";

sdks/java/extensions/kafka-factories/src/test/java/org/apache/beam/sdk/extensions/kafka/factories/KerberosConsumerFactoryFnTest.java

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public class KerberosConsumerFactoryFnTest {
4949
private KerberosConsumerFactoryFn factory;
5050
private String originalKrb5Conf;
5151
private static final String KRB5_GCS_PATH = "gs://sec-bucket/kerberos/krb5.conf";
52+
private static final String KRB5_S3_PATH = "s3://sec-bucket/kerberos/krb5.conf";
5253
private static final String LOCAL_FACTORY_TYPE = "kerberos";
5354

5455
@Before
@@ -61,8 +62,6 @@ public void setup() {
6162
} catch (Exception e) {
6263
throw new RuntimeException(e);
6364
}
64-
65-
factory = spy(new KerberosConsumerFactoryFn(KRB5_GCS_PATH));
6665
originalKrb5Conf = System.getProperty("java.security.krb5.conf");
6766
}
6867

@@ -86,8 +85,7 @@ public void tearDown() throws IOException {
8685

8786
@Test
8887
@SuppressWarnings("rawtypes")
89-
public void testHappyPath() {
90-
// Arrange
88+
public void testHappyGcsPath() {
9189
String keytabGcsPath = "gs://sec-bucket/keytabs/my.keytab";
9290
String expectedKrb5LocalPath = "/tmp/kerberos/krb5.conf";
9391
String expectedKeytabLocalPath = "/tmp/kerberos/sec-bucket/keytabs/my.keytab";
@@ -99,6 +97,69 @@ public void testHappyPath() {
9997
+ keytabGcsPath
10098
+ "\" principal=\"user@REALM\";");
10199

100+
factory = spy(new KerberosConsumerFactoryFn(KRB5_GCS_PATH));
101+
try (MockedStatic<FileAwareFactoryFn> mockedStaticFactory =
102+
Mockito.mockStatic(FileAwareFactoryFn.class);
103+
MockedStatic<Configuration> mockedConfiguration = Mockito.mockStatic(Configuration.class);
104+
MockedStatic<Files> mockedFiles = Mockito.mockStatic(Files.class);
105+
MockedConstruction<KafkaConsumer> mockedConsumer =
106+
Mockito.mockConstruction(KafkaConsumer.class)) {
107+
108+
Assert.assertNotNull(mockedConsumer);
109+
// Mock the static downloadExternalFile method to prevent any GCS interaction
110+
mockedStaticFactory
111+
.when(() -> FileAwareFactoryFn.downloadExternalFile(KRB5_GCS_PATH, expectedKrb5LocalPath))
112+
.thenReturn(expectedKrb5LocalPath);
113+
mockedStaticFactory
114+
.when(
115+
() -> FileAwareFactoryFn.downloadExternalFile(keytabGcsPath, expectedKeytabLocalPath))
116+
.thenReturn(expectedKeytabLocalPath);
117+
118+
Configuration mockConf = Mockito.mock(Configuration.class);
119+
mockedConfiguration.when(Configuration::getConfiguration).thenReturn(mockConf);
120+
mockedFiles
121+
.when(
122+
() ->
123+
Files.setPosixFilePermissions(
124+
ArgumentMatchers.any(Path.class), ArgumentMatchers.any(Set.class)))
125+
.thenReturn(null);
126+
mockedFiles
127+
.when(() -> Files.createDirectories(ArgumentMatchers.any(Path.class)))
128+
.thenReturn(null);
129+
130+
// Act
131+
factory.apply(config);
132+
133+
// Assert
134+
// 1. Verify that the krb5.conf system property was set correctly.
135+
Assert.assertEquals(expectedKrb5LocalPath, System.getProperty("java.security.krb5.conf"));
136+
137+
// 2. Capture the config passed to createObject and verify the keytab path was replaced.
138+
ArgumentCaptor<Map<String, Object>> configCaptor = ArgumentCaptor.forClass(Map.class);
139+
Mockito.verify(factory).createObject(configCaptor.capture());
140+
Map<String, Object> capturedConfig = configCaptor.getValue();
141+
String processedJaasConfig = (String) capturedConfig.get("sasl.jaas.config");
142+
Assert.assertTrue(processedJaasConfig.contains("keyTab=\"" + expectedKeytabLocalPath + "\""));
143+
144+
// 3. Verify that the JAAS configuration was refreshed.
145+
Mockito.verify(mockConf).refresh();
146+
}
147+
}
148+
149+
@Test
150+
@SuppressWarnings("rawtypes")
151+
public void testHappyS3Path() {
152+
String keytabPath = "s3://sec-bucket/keytabs/my.keytab";
153+
String expectedKrb5LocalPath = "/tmp/kerberos/krb5.conf";
154+
String expectedKeytabLocalPath = "/tmp/kerberos/sec-bucket/keytabs/my.keytab";
155+
156+
Map<String, Object> config = new HashMap<>();
157+
config.put(
158+
"sasl.jaas.config",
159+
"com.sun.security.auth.module.Krb5LoginModule required keyTab=\""
160+
+ keytabPath
161+
+ "\" principal=\"user@REALM\";");
162+
factory = spy(new KerberosConsumerFactoryFn(KRB5_S3_PATH));
102163
try (MockedStatic<FileAwareFactoryFn> mockedStaticFactory =
103164
Mockito.mockStatic(FileAwareFactoryFn.class);
104165
MockedStatic<Configuration> mockedConfiguration = Mockito.mockStatic(Configuration.class);
@@ -107,12 +168,12 @@ public void testHappyPath() {
107168
Mockito.mockConstruction(KafkaConsumer.class)) {
108169

109170
Assert.assertNotNull(mockedConsumer);
110-
// Mock the static downloadGcsFile method to prevent any GCS interaction
171+
// Mock the static downloadExternalFile method to prevent any GCS interaction
111172
mockedStaticFactory
112-
.when(() -> FileAwareFactoryFn.downloadGcsFile(KRB5_GCS_PATH, expectedKrb5LocalPath))
173+
.when(() -> FileAwareFactoryFn.downloadExternalFile(KRB5_S3_PATH, expectedKrb5LocalPath))
113174
.thenReturn(expectedKrb5LocalPath);
114175
mockedStaticFactory
115-
.when(() -> FileAwareFactoryFn.downloadGcsFile(keytabGcsPath, expectedKeytabLocalPath))
176+
.when(() -> FileAwareFactoryFn.downloadExternalFile(keytabPath, expectedKeytabLocalPath))
116177
.thenReturn(expectedKeytabLocalPath);
117178

118179
Configuration mockConf = Mockito.mock(Configuration.class);

0 commit comments

Comments
 (0)