Skip to content

Commit 5afde06

Browse files
committed
Migrate to a new module such that kafka remains GCP Agnostic.
1 parent c058a95 commit 5afde06

8 files changed

Lines changed: 141 additions & 184 deletions

File tree

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ tasks.register("javaioPreCommit") {
349349
dependsOn(":sdks:java:io:jms:build")
350350
dependsOn(":sdks:java:io:kafka:build")
351351
dependsOn(":sdks:java:io:kafka:upgrade:build")
352+
dependsOn(":sdks:java:io:kafka:file-aware-factories:build")
352353
dependsOn(":sdks:java:io:kudu:build")
353354
dependsOn(":sdks:java:io:mongodb:build")
354355
dependsOn(":sdks:java:io:mqtt:build")
Lines changed: 42 additions & 0 deletions
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+
* 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+
plugins { id 'org.apache.beam.module' }
20+
applyJavaNature(
21+
automaticModuleName: 'org.apache.beam.sdk.io.kafka.file.aware.factories',
22+
publish: 'False'
23+
)
24+
25+
description = "Apache Beam :: SDKs :: Java :: IO :: Kafka :: File :: Aware :: Factories"
26+
ext.summary = "Library to instantiate kafka clients with files from GCS or SecretManager."
27+
28+
dependencies {
29+
// ------------------------- CORE DEPENDENCIES -------------------------
30+
implementation project(path: ":sdks:java:core", configuration: "shadow")
31+
provided library.java.kafka_clients
32+
implementation 'com.google.cloud:google-cloud-secretmanager:2.72.0'
33+
implementation library.java.slf4j_api
34+
implementation library.java.vendored_guava_32_1_2_jre
35+
36+
// ------------------------- TEST DEPENDENCIES -------------------------
37+
testImplementation 'org.apache.kafka:kafka-clients:3.9.0'
38+
testImplementation library.java.junit
39+
testImplementation library.java.mockito_core
40+
testRuntimeOnly library.java.mockito_inline
41+
testImplementation project(path: ":sdks:java:core", configuration: "shadowTest")
42+
}

sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/FileAwareFactoryFn.java renamed to sdks/java/io/kafka/file-aware-factories/src/main/java/org/apache/beam/sdk/io/kafka/file/aware/factories/FileAwareFactoryFn.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* See the License for the specific language governing permissions and
1616
* limitations under the License.
1717
*/
18-
package org.apache.beam.sdk.io.kafka;
18+
package org.apache.beam.sdk.io.kafka.file.aware.factories;
1919

2020
/*
2121
* Licensed to the Apache Software Foundation (ASF) under one
@@ -196,9 +196,9 @@ protected byte[] getSecretWithCache(String secretId) {
196196
* @return a string with all instances of GCS paths converted to the local paths where the files
197197
* sit.
198198
*/
199-
private String replacePathWithLocal(String gcsPath) throws IOException {
200-
return DIRECTORY_PREFIX + "/" + factoryType + "/" + gcsPath.substring(GCS_PATH_PREFIX.length());
201-
}
199+
private String replacePathWithLocal(String gcsPath) throws IOException {
200+
return DIRECTORY_PREFIX + "/" + factoryType + "/" + gcsPath.substring(GCS_PATH_PREFIX.length());
201+
}
202202

203203
/**
204204
* @throws IOException A hook for subclasses to download and process specific files before the

sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KerberosConsumerFactoryFn.java renamed to sdks/java/io/kafka/file-aware-factories/src/main/java/org/apache/beam/sdk/io/kafka/file/aware/factories/KerberosConsumerFactoryFn.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* See the License for the specific language governing permissions and
1616
* limitations under the License.
1717
*/
18-
package org.apache.beam.sdk.io.kafka;
18+
package org.apache.beam.sdk.io.kafka.file.aware.factories;
1919

2020
import java.io.File;
2121
import java.io.IOException;

sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/FileAwareFactoryFnTest.java renamed to sdks/java/io/kafka/file-aware-factories/src/test/java/org/apache/beam/sdk/io/kafka/file/aware/factories/FileAwareFactoryFnTest.java

Lines changed: 44 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,7 @@
1515
* See the License for the specific language governing permissions and
1616
* limitations under the License.
1717
*/
18-
package org.apache.beam.sdk.io.kafka;
19-
20-
import static org.junit.Assert.assertEquals;
21-
import static org.junit.Assert.assertThrows;
22-
import static org.junit.Assert.assertTrue;
23-
import static org.mockito.ArgumentMatchers.any;
24-
import static org.mockito.ArgumentMatchers.eq;
25-
import static org.mockito.Mockito.doReturn;
26-
import static org.mockito.Mockito.mock;
27-
import static org.mockito.Mockito.mockStatic;
28-
import static org.mockito.Mockito.spy;
29-
import static org.mockito.Mockito.when;
18+
package org.apache.beam.sdk.io.kafka.file.aware.factories;
3019

3120
import java.io.ByteArrayInputStream;
3221
import java.io.File;
@@ -42,13 +31,16 @@
4231
import org.apache.beam.sdk.io.FileSystems;
4332
import org.apache.beam.sdk.io.fs.MatchResult;
4433
import org.apache.beam.sdk.io.fs.ResourceId;
34+
import org.junit.Assert;
4535
import org.junit.Before;
4636
import org.junit.Rule;
4737
import org.junit.Test;
4838
import org.junit.rules.TemporaryFolder;
4939
import org.junit.runner.RunWith;
5040
import org.junit.runners.JUnit4;
41+
import org.mockito.ArgumentMatchers;
5142
import org.mockito.MockedStatic;
43+
import org.mockito.Mockito;
5244

5345
@RunWith(JUnit4.class)
5446
public class FileAwareFactoryFnTest {
@@ -75,8 +67,8 @@ protected Object createObject(Map<String, Object> config) {
7567
@Before
7668
public void setup() throws IOException {
7769
baseDir = "/tmp/" + TEST_FACTORY_TYPE;
78-
factory = spy(new TestFactoryFn());
79-
doReturn(baseDir).when(factory).getBaseDirectory();
70+
factory = Mockito.spy(new TestFactoryFn());
71+
Mockito.doReturn(baseDir).when(factory).getBaseDirectory();
8072
}
8173

8274
@Test
@@ -93,11 +85,11 @@ public void testHappyPathReplacesGcsPath() {
9385

9486
// Act & Assert
9587
// Use try-with-resources to manage the scope of the static mock on FileSystems
96-
try (MockedStatic<FileSystems> mockedFileSystems = mockStatic(FileSystems.class)) {
88+
try (MockedStatic<FileSystems> mockedFileSystems = Mockito.mockStatic(FileSystems.class)) {
9789
// 1. Mock the underlying static FileSystems calls to avoid real network I/O
98-
MatchResult.Metadata metadata = mock(MatchResult.Metadata.class);
99-
ResourceId resourceId = mock(ResourceId.class);
100-
when(metadata.resourceId()).thenReturn(resourceId);
90+
MatchResult.Metadata metadata = Mockito.mock(MatchResult.Metadata.class);
91+
ResourceId resourceId = Mockito.mock(ResourceId.class);
92+
Mockito.when(metadata.resourceId()).thenReturn(resourceId);
10193
mockedFileSystems.when(() -> FileSystems.matchSingleFileSpec(gcsPath)).thenReturn(metadata);
10294

10395
// 2. Mock 'open' to return a channel with no data, simulating a successful download
@@ -108,8 +100,9 @@ public void testHappyPathReplacesGcsPath() {
108100
Map<String, Object> processedConfig = (Map<String, Object>) factory.apply(config);
109101

110102
// Assert
111-
assertEquals(expectedLocalPath, processedConfig.get("config.file.path"));
112-
assertTrue("Local file should have been created", new File(expectedLocalPath).exists());
103+
Assert.assertEquals(expectedLocalPath, processedConfig.get("config.file.path"));
104+
Assert.assertTrue(
105+
"Local file should have been created", new File(expectedLocalPath).exists());
113106
}
114107
}
115108

@@ -121,17 +114,17 @@ public void testApplyFailurePathThrowsRuntimeExceptionOnDownloadFailure() {
121114
config.put("critical.file", gcsPath);
122115

123116
// Mock the static FileSystems.matchSingleFileSpec to throw an exception
124-
try (MockedStatic<FileSystems> mockedFileSystems = mockStatic(FileSystems.class)) {
117+
try (MockedStatic<FileSystems> mockedFileSystems = Mockito.mockStatic(FileSystems.class)) {
125118
mockedFileSystems
126119
.when(() -> FileSystems.matchSingleFileSpec(gcsPath))
127120
.thenThrow(new IOException("GCS file not found"));
128121

129122
// Act & Assert
130123
RuntimeException exception =
131-
assertThrows(RuntimeException.class, () -> factory.apply(config));
132-
assertTrue(exception.getMessage().contains("Failed trying to process value"));
133-
assertTrue(exception.getCause() instanceof IOException);
134-
assertTrue(exception.getCause().getMessage().contains("Failed to download file"));
124+
Assert.assertThrows(RuntimeException.class, () -> factory.apply(config));
125+
Assert.assertTrue(exception.getMessage().contains("Failed trying to process value"));
126+
Assert.assertTrue(exception.getCause() instanceof IOException);
127+
Assert.assertTrue(exception.getCause().getMessage().contains("Failed to download file"));
135128
}
136129
}
137130

@@ -147,7 +140,7 @@ public void testApplyHappyPathIgnoresNonGcsValues() {
147140
Map<String, Object> processedConfig = (Map<String, Object>) factory.apply(config);
148141

149142
// Assert
150-
assertEquals(config, processedConfig);
143+
Assert.assertEquals(config, processedConfig);
151144
}
152145

153146
@Test
@@ -172,7 +165,7 @@ public void testApplyEdgeCaseMultipleGcsPathsInSingleValue() {
172165
Map<String, Object> config = new HashMap<>();
173166
config.put("jaas.config", originalValue);
174167

175-
try (MockedStatic<FileSystems> mockedFileSystems = mockStatic(FileSystems.class)) {
168+
try (MockedStatic<FileSystems> mockedFileSystems = Mockito.mockStatic(FileSystems.class)) {
176169
// Mock GCS calls for both paths
177170
mockSuccessfulDownload(mockedFileSystems, gcsPath1);
178171
mockSuccessfulDownload(mockedFileSystems, gcsPath2);
@@ -181,7 +174,7 @@ public void testApplyEdgeCaseMultipleGcsPathsInSingleValue() {
181174
Map<String, Object> processedConfig = (Map<String, Object>) factory.apply(config);
182175

183176
// Assert
184-
assertEquals(expectedProcessedValue, processedConfig.get("jaas.config"));
177+
Assert.assertEquals(expectedProcessedValue, processedConfig.get("jaas.config"));
185178
}
186179
}
187180

@@ -193,22 +186,25 @@ public void testApplyEdgeCaseLocalFileWriteFails() throws IOException {
193186
config.put("a.file", gcsPath);
194187

195188
// Mock GCS part to succeed
196-
try (MockedStatic<FileSystems> mockedFileSystems = mockStatic(FileSystems.class);
197-
MockedStatic<FileChannel> mockedFileChannel = mockStatic(FileChannel.class)) {
189+
try (MockedStatic<FileSystems> mockedFileSystems = Mockito.mockStatic(FileSystems.class);
190+
MockedStatic<FileChannel> mockedFileChannel = Mockito.mockStatic(FileChannel.class)) {
198191
mockSuccessfulDownload(mockedFileSystems, gcsPath);
199192

200193
// Mock the local file writing part to fail
201194
mockedFileChannel
202-
.when(() -> FileChannel.open(any(Path.class), any(Set.class)))
195+
.when(
196+
() ->
197+
FileChannel.open(
198+
ArgumentMatchers.any(Path.class), ArgumentMatchers.any(Set.class)))
203199
.thenThrow(new IOException("Permission denied"));
204200

205201
// Act & Assert
206202
RuntimeException exception =
207-
assertThrows(RuntimeException.class, () -> factory.apply(config));
208-
assertTrue(exception.getMessage().contains("Failed trying to process value"));
209-
assertTrue(exception.getCause() instanceof IOException);
203+
Assert.assertThrows(RuntimeException.class, () -> factory.apply(config));
204+
Assert.assertTrue(exception.getMessage().contains("Failed trying to process value"));
205+
Assert.assertTrue(exception.getCause() instanceof IOException);
210206
// Check that the root cause is our "Permission denied" mock
211-
assertTrue(exception.getCause().getCause().getMessage().contains("Permission denied"));
207+
Assert.assertTrue(exception.getCause().getCause().getMessage().contains("Permission denied"));
212208
}
213209
}
214210

@@ -233,7 +229,7 @@ public void testApplyHappyPathResolvesSecretValue() {
233229
@Override
234230
public byte[] getSecret(String secretIdentifier) {
235231
// Assert that the correct identifier is passed
236-
assertEquals(secretVersionParsed, secretIdentifier);
232+
Assert.assertEquals(secretVersionParsed, secretIdentifier);
237233
// Return a predictable, hardcoded value for the test
238234
return secretValue.getBytes(StandardCharsets.UTF_8);
239235
}
@@ -245,7 +241,7 @@ public byte[] getSecret(String secretIdentifier) {
245241
(Map<String, Object>) factoryWithMockedSecret.apply(config);
246242

247243
// Assert
248-
assertEquals(expectedProcessedValue, processedConfig.get("db.password"));
244+
Assert.assertEquals(expectedProcessedValue, processedConfig.get("db.password"));
249245
}
250246

251247
@Test
@@ -258,18 +254,22 @@ public void testApplyFailurePathThrowsExceptionForInvalidSecretFormat() {
258254
// Act & Assert
259255
// The spy will call the real method here, which will throw an exception
260256
// because the secret path is not parsable.
261-
RuntimeException ex = assertThrows(RuntimeException.class, () -> factory.apply(config));
262-
assertEquals(IllegalArgumentException.class, ex.getCause().getClass());
257+
RuntimeException ex = Assert.assertThrows(RuntimeException.class, () -> factory.apply(config));
258+
Assert.assertEquals(IllegalArgumentException.class, ex.getCause().getClass());
263259
}
264260

265261
// Helper method to reduce boilerplate in mocking successful GCS downloads
266262
private void mockSuccessfulDownload(MockedStatic<FileSystems> mockedFileSystems, String gcsPath) {
267-
MatchResult.Metadata metadata = mock(MatchResult.Metadata.class);
268-
ResourceId resourceId = mock(ResourceId.class);
269-
when(metadata.resourceId()).thenReturn(resourceId);
270-
mockedFileSystems.when(() -> FileSystems.matchSingleFileSpec(eq(gcsPath))).thenReturn(metadata);
263+
MatchResult.Metadata metadata = Mockito.mock(MatchResult.Metadata.class);
264+
ResourceId resourceId = Mockito.mock(ResourceId.class);
265+
Mockito.when(metadata.resourceId()).thenReturn(resourceId);
266+
mockedFileSystems
267+
.when(() -> FileSystems.matchSingleFileSpec(ArgumentMatchers.eq(gcsPath)))
268+
.thenReturn(metadata);
271269

272270
ReadableByteChannel channel = Channels.newChannel(new ByteArrayInputStream(new byte[0]));
273-
mockedFileSystems.when(() -> FileSystems.open(eq(resourceId))).thenReturn(channel);
271+
mockedFileSystems
272+
.when(() -> FileSystems.open(ArgumentMatchers.eq(resourceId)))
273+
.thenReturn(channel);
274274
}
275275
}

sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/KerberosConsumerFactoryFnTest.java renamed to sdks/java/io/kafka/file-aware-factories/src/test/java/org/apache/beam/sdk/io/kafka/file/aware/factories/KerberosConsumerFactoryFnTest.java

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,9 @@
1515
* See the License for the specific language governing permissions and
1616
* limitations under the License.
1717
*/
18-
package org.apache.beam.sdk.io.kafka;
19-
20-
import static org.junit.Assert.assertEquals;
21-
import static org.junit.Assert.assertNotNull;
22-
import static org.junit.Assert.assertTrue;
23-
import static org.mockito.ArgumentMatchers.any;
24-
import static org.mockito.Mockito.mock;
25-
import static org.mockito.Mockito.mockConstruction;
26-
import static org.mockito.Mockito.mockStatic;
18+
package org.apache.beam.sdk.io.kafka.file.aware.factories;
19+
2720
import static org.mockito.Mockito.spy;
28-
import static org.mockito.Mockito.verify;
2921

3022
import java.io.File;
3123
import java.io.IOException;
@@ -40,13 +32,16 @@
4032
import javax.security.auth.login.Configuration;
4133
import org.apache.kafka.clients.consumer.KafkaConsumer;
4234
import org.junit.After;
35+
import org.junit.Assert;
4336
import org.junit.Before;
4437
import org.junit.Test;
4538
import org.junit.runner.RunWith;
4639
import org.junit.runners.JUnit4;
4740
import org.mockito.ArgumentCaptor;
41+
import org.mockito.ArgumentMatchers;
4842
import org.mockito.MockedConstruction;
4943
import org.mockito.MockedStatic;
44+
import org.mockito.Mockito;
5045

5146
@RunWith(JUnit4.class)
5247
public class KerberosConsumerFactoryFnTest {
@@ -110,12 +105,13 @@ public void testHappyPath() {
110105
// Use try-with-resources for all necessary static mocks
111106

112107
try (MockedStatic<FileAwareFactoryFn> mockedStaticFactory =
113-
mockStatic(FileAwareFactoryFn.class);
114-
MockedStatic<Configuration> mockedConfiguration = mockStatic(Configuration.class);
115-
MockedStatic<Files> mockedFiles = mockStatic(Files.class);
116-
MockedConstruction<KafkaConsumer> mockedConsumer = mockConstruction(KafkaConsumer.class)) {
108+
Mockito.mockStatic(FileAwareFactoryFn.class);
109+
MockedStatic<Configuration> mockedConfiguration = Mockito.mockStatic(Configuration.class);
110+
MockedStatic<Files> mockedFiles = Mockito.mockStatic(Files.class);
111+
MockedConstruction<KafkaConsumer> mockedConsumer =
112+
Mockito.mockConstruction(KafkaConsumer.class)) {
117113

118-
assertNotNull(mockedConsumer);
114+
Assert.assertNotNull(mockedConsumer);
119115
// Mock the static downloadGcsFile method to prevent any GCS interaction
120116
mockedStaticFactory
121117
.when(() -> FileAwareFactoryFn.downloadGcsFile(KRB5_GCS_PATH, expectedKrb5LocalPath))
@@ -125,29 +121,34 @@ public void testHappyPath() {
125121
.thenReturn(expectedKeytabLocalPath);
126122

127123
// Mock other dependencies to prevent side effects
128-
Configuration mockConf = mock(Configuration.class);
124+
Configuration mockConf = Mockito.mock(Configuration.class);
129125
mockedConfiguration.when(Configuration::getConfiguration).thenReturn(mockConf);
130126
mockedFiles
131-
.when(() -> Files.setPosixFilePermissions(any(Path.class), any(Set.class)))
127+
.when(
128+
() ->
129+
Files.setPosixFilePermissions(
130+
ArgumentMatchers.any(Path.class), ArgumentMatchers.any(Set.class)))
131+
.thenReturn(null);
132+
mockedFiles
133+
.when(() -> Files.createDirectories(ArgumentMatchers.any(Path.class)))
132134
.thenReturn(null);
133-
mockedFiles.when(() -> Files.createDirectories(any(Path.class))).thenReturn(null);
134135

135136
// Act
136137
factory.apply(config);
137138

138139
// Assert
139140
// 1. Verify that the krb5.conf system property was set correctly.
140-
assertEquals(expectedKrb5LocalPath, System.getProperty("java.security.krb5.conf"));
141+
Assert.assertEquals(expectedKrb5LocalPath, System.getProperty("java.security.krb5.conf"));
141142

142143
// 2. Capture the config passed to createObject and verify the keytab path was replaced.
143144
ArgumentCaptor<Map<String, Object>> configCaptor = ArgumentCaptor.forClass(Map.class);
144-
verify(factory).createObject(configCaptor.capture());
145+
Mockito.verify(factory).createObject(configCaptor.capture());
145146
Map<String, Object> capturedConfig = configCaptor.getValue();
146147
String processedJaasConfig = (String) capturedConfig.get("sasl.jaas.config");
147-
assertTrue(processedJaasConfig.contains("keyTab=\"" + expectedKeytabLocalPath + "\""));
148+
Assert.assertTrue(processedJaasConfig.contains("keyTab=\"" + expectedKeytabLocalPath + "\""));
148149

149150
// 3. Verify that the JAAS configuration was refreshed.
150-
verify(mockConf).refresh();
151+
Mockito.verify(mockConf).refresh();
151152
}
152153
}
153154
}

0 commit comments

Comments
 (0)