Skip to content

Commit c4b93ee

Browse files
committed
remove merge conflict code.
1 parent 041d5d4 commit c4b93ee

5 files changed

Lines changed: 467 additions & 54 deletions

File tree

sdks/java/extensions/kafka-factories/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ dependencies {
3232
implementation 'com.google.cloud:google-cloud-secretmanager:2.72.0'
3333
implementation library.java.slf4j_api
3434
implementation library.java.vendored_guava_32_1_2_jre
35-
implementation 'com.google.cloud:google-cloud-storage:2.57.0'
35+
implementation project(path: ":sdks:java:extensions:google-cloud-platform-core")
36+
permitUnusedDeclared project(path: ":sdks:java:extensions:google-cloud-platform-core")
3637
// ------------------------- TEST DEPENDENCIES -------------------------
3738
testImplementation 'org.apache.kafka:kafka-clients:3.9.0'
3839
testImplementation library.java.junit

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

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,23 @@
2020
import com.google.cloud.secretmanager.v1.AccessSecretVersionResponse;
2121
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
2222
import com.google.cloud.secretmanager.v1.SecretVersionName;
23-
import com.google.cloud.storage.Blob;
24-
import com.google.cloud.storage.BlobId;
25-
import com.google.cloud.storage.Storage;
26-
import com.google.cloud.storage.StorageException;
27-
import com.google.cloud.storage.StorageOptions;
23+
import java.io.File;
2824
import java.io.IOException;
25+
import java.nio.channels.FileChannel;
26+
import java.nio.channels.ReadableByteChannel;
2927
import java.nio.charset.StandardCharsets;
3028
import java.nio.file.Files;
3129
import java.nio.file.Path;
3230
import java.nio.file.Paths;
31+
import java.nio.file.StandardOpenOption;
3332
import java.util.HashMap;
33+
import java.util.HashSet;
3434
import java.util.Map;
35+
import java.util.Set;
3536
import java.util.concurrent.ConcurrentHashMap;
3637
import java.util.regex.Matcher;
3738
import java.util.regex.Pattern;
39+
import org.apache.beam.sdk.io.FileSystems;
3840
import org.apache.beam.sdk.transforms.SerializableFunction;
3941
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions;
4042
import org.slf4j.Logger;
@@ -139,33 +141,27 @@ public T apply(Map<String, Object> config) {
139141
*/
140142
protected static synchronized String downloadGcsFile(String gcsFilePath, String outputFileString)
141143
throws IOException {
142-
Path outputFilePath = Paths.get(outputFileString);
143-
// 1. Check if the file already exists to avoid doing duplicate work
144-
if (Files.exists(outputFilePath)) {
145-
System.out.println("File " + outputFileString + " already exists, skipping download.");
144+
// create the file only if it doesn't exist
145+
if (new File(outputFileString).exists()) {
146146
return outputFileString;
147147
}
148-
if (outputFilePath.getParent() != null) {
149-
Files.createDirectories(outputFilePath.getParent());
148+
Path outputFilePath = Paths.get(outputFileString);
149+
Path parentDir = outputFilePath.getParent();
150+
if (parentDir != null) {
151+
Files.createDirectories(parentDir);
150152
}
151-
152-
try {
153-
BlobId blobId = BlobId.fromGsUtilUri(gcsFilePath);
154-
Storage storage = StorageOptions.getDefaultInstance().getService();
155-
Blob blob = storage.get(blobId);
156-
if (blob == null) {
157-
throw new IOException("File not found at GCS path: " + gcsFilePath);
153+
LOG.info("Staging GCS file [{}] to [{}]", gcsFilePath, outputFileString);
154+
Set<StandardOpenOption> options = new HashSet<>(2);
155+
options.add(StandardOpenOption.CREATE);
156+
options.add(StandardOpenOption.WRITE);
157+
158+
// Copy the GCS file into a local file and will throw an I/O exception in case file not found.
159+
try (ReadableByteChannel readerChannel =
160+
FileSystems.open(FileSystems.matchSingleFileSpec(gcsFilePath).resourceId())) {
161+
try (FileChannel writeChannel = FileChannel.open(outputFilePath, options)) {
162+
writeChannel.transferFrom(readerChannel, 0, Long.MAX_VALUE);
158163
}
159-
160-
// 2. Download the blob's content to the specified local file
161-
blob.downloadTo(outputFilePath);
162-
163-
System.out.println("Downloaded " + gcsFilePath + " to " + outputFileString);
164-
} catch (StorageException e) {
165-
// Catch the StorageException and re-throw as a RuntimeException
166-
throw new RuntimeException("Failed to download file from GCS: " + e.getMessage(), e);
167164
}
168-
169165
return outputFileString;
170166
}
171167

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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+
package org.apache.beam.sdk.extensions.kafka.factories;
19+
20+
import java.io.ByteArrayInputStream;
21+
import java.io.File;
22+
import java.io.IOException;
23+
import java.nio.channels.Channels;
24+
import java.nio.channels.FileChannel;
25+
import java.nio.channels.ReadableByteChannel;
26+
import java.nio.charset.StandardCharsets;
27+
import java.nio.file.Path;
28+
import java.util.HashMap;
29+
import java.util.Map;
30+
import java.util.Set;
31+
import org.apache.beam.sdk.io.FileSystems;
32+
import org.apache.beam.sdk.io.fs.MatchResult;
33+
import org.apache.beam.sdk.io.fs.ResourceId;
34+
import org.junit.Assert;
35+
import org.junit.Before;
36+
import org.junit.Rule;
37+
import org.junit.Test;
38+
import org.junit.rules.TemporaryFolder;
39+
import org.junit.runner.RunWith;
40+
import org.junit.runners.JUnit4;
41+
import org.mockito.ArgumentMatchers;
42+
import org.mockito.MockedStatic;
43+
import org.mockito.Mockito;
44+
45+
@RunWith(JUnit4.class)
46+
public class FileAwareFactoryFnTest {
47+
48+
@Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
49+
50+
private TestFactoryFn factory;
51+
private String baseDir;
52+
private static final String TEST_FACTORY_TYPE = "test-factory";
53+
54+
// A concrete implementation for testing the abstract FileAwareFactoryFn
55+
static class TestFactoryFn extends FileAwareFactoryFn<Object> {
56+
public TestFactoryFn() {
57+
super(TEST_FACTORY_TYPE);
58+
}
59+
60+
@Override
61+
protected Object createObject(Map<String, Object> config) {
62+
// Return the processed config for easy assertion
63+
return config;
64+
}
65+
}
66+
67+
@Before
68+
public void setup() throws IOException {
69+
baseDir = "/tmp/" + TEST_FACTORY_TYPE;
70+
factory = Mockito.spy(new TestFactoryFn());
71+
Mockito.doReturn(baseDir).when(factory).getBaseDirectory();
72+
}
73+
74+
@Test
75+
public void testHappyPathReplacesGcsPath() {
76+
// Arrange
77+
String gcsPath = "gs://test-bucket/config-file.json";
78+
String expectedLocalPath =
79+
FileAwareFactoryFn.DIRECTORY_PREFIX
80+
+ "/"
81+
+ TEST_FACTORY_TYPE
82+
+ "/test-bucket/config-file.json";
83+
Map<String, Object> config = new HashMap<>();
84+
config.put("config.file.path", gcsPath);
85+
86+
// Act & Assert
87+
// Use try-with-resources to manage the scope of the static mock on FileSystems
88+
try (MockedStatic<FileSystems> mockedFileSystems = Mockito.mockStatic(FileSystems.class)) {
89+
// 1. Mock the underlying static FileSystems calls to avoid real network I/O
90+
MatchResult.Metadata metadata = Mockito.mock(MatchResult.Metadata.class);
91+
ResourceId resourceId = Mockito.mock(ResourceId.class);
92+
Mockito.when(metadata.resourceId()).thenReturn(resourceId);
93+
mockedFileSystems.when(() -> FileSystems.matchSingleFileSpec(gcsPath)).thenReturn(metadata);
94+
95+
// 2. Mock 'open' to return a channel with no data, simulating a successful download
96+
ReadableByteChannel channel = Channels.newChannel(new ByteArrayInputStream(new byte[0]));
97+
mockedFileSystems.when(() -> FileSystems.open(resourceId)).thenReturn(channel);
98+
99+
// Act
100+
Map<String, Object> processedConfig = (Map<String, Object>) factory.apply(config);
101+
102+
// Assert
103+
Assert.assertEquals(expectedLocalPath, processedConfig.get("config.file.path"));
104+
Assert.assertTrue(
105+
"Local file should have been created", new File(expectedLocalPath).exists());
106+
}
107+
}
108+
109+
@Test
110+
public void testApplyFailurePathThrowsRuntimeExceptionOnDownloadFailure() {
111+
// Arrange
112+
String gcsPath = "gs://test-bucket/failing-file.txt";
113+
Map<String, Object> config = new HashMap<>();
114+
config.put("critical.file", gcsPath);
115+
116+
// Mock the static FileSystems.matchSingleFileSpec to throw an exception
117+
try (MockedStatic<FileSystems> mockedFileSystems = Mockito.mockStatic(FileSystems.class)) {
118+
mockedFileSystems
119+
.when(() -> FileSystems.matchSingleFileSpec(gcsPath))
120+
.thenThrow(new IOException("GCS file not found"));
121+
122+
// Act & Assert
123+
RuntimeException exception =
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"));
128+
}
129+
}
130+
131+
@Test
132+
public void testApplyHappyPathIgnoresNonGcsValues() {
133+
// Arrange
134+
Map<String, Object> config = new HashMap<>();
135+
config.put("some.string", "/local/path/file.txt");
136+
config.put("some.number", 42);
137+
config.put("some.boolean", false);
138+
139+
// Act
140+
Map<String, Object> processedConfig = (Map<String, Object>) factory.apply(config);
141+
142+
// Assert
143+
Assert.assertEquals(config, processedConfig);
144+
}
145+
146+
@Test
147+
public void testApplyEdgeCaseMultipleGcsPathsInSingleValue() {
148+
// Arrange
149+
String gcsPath1 = "gs://bucket/keytab.keytab";
150+
String gcsPath2 = "gs://bucket/trust.jks";
151+
String originalValue =
152+
"jaas_config keyTab=\"" + gcsPath1 + "\" trustStore=\"" + gcsPath2 + "\"";
153+
154+
String expectedLocalPath1 =
155+
FileAwareFactoryFn.DIRECTORY_PREFIX + "/" + TEST_FACTORY_TYPE + "/bucket/keytab.keytab";
156+
String expectedLocalPath2 =
157+
FileAwareFactoryFn.DIRECTORY_PREFIX + "/" + TEST_FACTORY_TYPE + "/bucket/trust.jks";
158+
String expectedProcessedValue =
159+
"jaas_config keyTab=\""
160+
+ expectedLocalPath1
161+
+ "\" trustStore=\""
162+
+ expectedLocalPath2
163+
+ "\"";
164+
165+
Map<String, Object> config = new HashMap<>();
166+
config.put("jaas.config", originalValue);
167+
168+
try (MockedStatic<FileSystems> mockedFileSystems = Mockito.mockStatic(FileSystems.class)) {
169+
// Mock GCS calls for both paths
170+
mockSuccessfulDownload(mockedFileSystems, gcsPath1);
171+
mockSuccessfulDownload(mockedFileSystems, gcsPath2);
172+
173+
// Act
174+
Map<String, Object> processedConfig = (Map<String, Object>) factory.apply(config);
175+
176+
// Assert
177+
Assert.assertEquals(expectedProcessedValue, processedConfig.get("jaas.config"));
178+
}
179+
}
180+
181+
@Test
182+
public void testApplyEdgeCaseLocalFileWriteFails() throws IOException {
183+
// Arrange
184+
String gcsPath = "gs://test-bucket/some-file.txt";
185+
Map<String, Object> config = new HashMap<>();
186+
config.put("a.file", gcsPath);
187+
188+
// Mock GCS part to succeed
189+
try (MockedStatic<FileSystems> mockedFileSystems = Mockito.mockStatic(FileSystems.class);
190+
MockedStatic<FileChannel> mockedFileChannel = Mockito.mockStatic(FileChannel.class)) {
191+
mockSuccessfulDownload(mockedFileSystems, gcsPath);
192+
193+
// Mock the local file writing part to fail
194+
mockedFileChannel
195+
.when(
196+
() ->
197+
FileChannel.open(
198+
ArgumentMatchers.any(Path.class), ArgumentMatchers.any(Set.class)))
199+
.thenThrow(new IOException("Permission denied"));
200+
201+
// Act & Assert
202+
RuntimeException exception =
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);
206+
// Check that the root cause is our "Permission denied" mock
207+
Assert.assertTrue(exception.getCause().getCause().getMessage().contains("Permission denied"));
208+
}
209+
}
210+
211+
@Test
212+
public void testApplyHappyPathResolvesSecretValue() {
213+
// Arrange
214+
String secretVersion = "secretValue:projects/p/secrets/s/versions/v";
215+
String secretVersionParsed = "projects/p/secrets/s/versions/v";
216+
String secretValue = "my-secret-password";
217+
String originalValue = "password=" + secretVersion;
218+
String expectedProcessedValue = "password=" + secretValue;
219+
220+
Map<String, Object> config = new HashMap<>();
221+
config.put("db.password", originalValue);
222+
223+
TestFactoryFn factoryWithMockedSecret =
224+
new TestFactoryFn() {
225+
@Override
226+
public byte[] getSecret(String secretIdentifier) {
227+
// Assert that the correct identifier is passed
228+
Assert.assertEquals(secretVersionParsed, secretIdentifier);
229+
// Return a predictable, hardcoded value for the test
230+
return secretValue.getBytes(StandardCharsets.UTF_8);
231+
}
232+
};
233+
234+
// Act
235+
@SuppressWarnings("unchecked")
236+
Map<String, Object> processedConfig =
237+
(Map<String, Object>) factoryWithMockedSecret.apply(config);
238+
239+
// Assert
240+
Assert.assertEquals(expectedProcessedValue, processedConfig.get("db.password"));
241+
}
242+
243+
@Test
244+
public void testApplyFailurePathThrowsExceptionForInvalidSecretFormat() {
245+
// Arrange
246+
String invalidSecret = "secretValue:not-a-valid-secret-path";
247+
Map<String, Object> config = new HashMap<>();
248+
config.put("db.password", "password=" + invalidSecret);
249+
250+
// Act & Assert
251+
RuntimeException ex = Assert.assertThrows(RuntimeException.class, () -> factory.apply(config));
252+
Assert.assertEquals(IllegalArgumentException.class, ex.getCause().getClass());
253+
}
254+
255+
// Helper method to reduce boilerplate in mocking successful GCS downloads
256+
private void mockSuccessfulDownload(MockedStatic<FileSystems> mockedFileSystems, String gcsPath) {
257+
MatchResult.Metadata metadata = Mockito.mock(MatchResult.Metadata.class);
258+
ResourceId resourceId = Mockito.mock(ResourceId.class);
259+
Mockito.when(metadata.resourceId()).thenReturn(resourceId);
260+
mockedFileSystems
261+
.when(() -> FileSystems.matchSingleFileSpec(ArgumentMatchers.eq(gcsPath)))
262+
.thenReturn(metadata);
263+
264+
ReadableByteChannel channel = Channels.newChannel(new ByteArrayInputStream(new byte[0]));
265+
mockedFileSystems
266+
.when(() -> FileSystems.open(ArgumentMatchers.eq(resourceId)))
267+
.thenReturn(channel);
268+
}
269+
}

0 commit comments

Comments
 (0)