This is an automated email from the ASF dual-hosted git repository.

david-streamlio pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar-connectors.git


The following commit(s) were added to refs/heads/master by this push:
     new c00efd9d [fix][io] Fix DynamoDB source client construction with a 
custom endpoint (#79)
c00efd9d is described below

commit c00efd9dd2f5fbcf60dee8ee69674d62d2328584
Author: David Kjerrumgaard <[email protected]>
AuthorDate: Fri Jul 10 06:57:43 2026 -0700

    [fix][io] Fix DynamoDB source client construction with a custom endpoint 
(#79)
    
    * [fix][io] Fix DynamoDB source client construction with a custom endpoint
    
    All three client builders set an endpoint configuration and then, in a
    separate branch, a region. AWS SDK v1 rejects that combination with
    'Only one of Region or EndpointConfiguration may be set', and
    DynamoDBSource.open() requires awsRegion, so every configuration that
    supplies an endpoint failed at client construction. The endpoint path
    could not have worked.
    
    Make region and endpoint mutually exclusive. Also gate the DynamoDB and
    CloudWatch builders on their own endpoint fields rather than on
    awsEndpoint: they were reading dynamoEndpoint and cloudwatchEndpoint
    while gating on awsEndpoint, so setting awsEndpoint alone built an
    endpoint configuration from an empty string.
    
    * [fix][io] Add regression tests for DynamoDB client endpoint/region 
handling
    
    Cover the crash directly: constructing each client with an endpoint and
    the (mandatory) region must not throw, each builder must select its own
    endpoint field, and setting awsEndpoint alone must not leak an empty
    endpoint into the DynamoDB and CloudWatch clients.
    
    Three of the four fail against the unfixed builders with the original
    IllegalStateException; the fourth pins the unaffected region-only path.
    
    * [fix][io] Restore the endpoint/region fix
    
    The regression tests added in the previous commit were committed
    alongside an accidental revert of DynamoDBSourceConfig, and correctly
    failed CI with the original IllegalStateException.
---
 .../pulsar/io/dynamodb/DynamoDBSourceConfig.java   |  15 +--
 .../DynamoDBSourceConfigClientBuilderTest.java     | 150 +++++++++++++++++++++
 2 files changed, 157 insertions(+), 8 deletions(-)

diff --git 
a/dynamodb/src/main/java/org/apache/pulsar/io/dynamodb/DynamoDBSourceConfig.java
 
b/dynamodb/src/main/java/org/apache/pulsar/io/dynamodb/DynamoDBSourceConfig.java
index 0547ff8f..ac7a0c30 100644
--- 
a/dynamodb/src/main/java/org/apache/pulsar/io/dynamodb/DynamoDBSourceConfig.java
+++ 
b/dynamodb/src/main/java/org/apache/pulsar/io/dynamodb/DynamoDBSourceConfig.java
@@ -184,11 +184,12 @@ public class DynamoDBSourceConfig implements Serializable 
{
     public AmazonDynamoDBStreams 
buildDynamoDBStreamsClient(AwsCredentialProviderPlugin credPlugin) {
         AmazonDynamoDBStreamsClientBuilder builder = 
AmazonDynamoDBStreamsClientBuilder.standard();
 
+        // EndpointConfiguration already carries the signing region, and the 
AWS SDK v1 forbids
+        // setting both an endpoint configuration and a region, so they must 
be mutually exclusive.
         if (!this.getAwsEndpoint().isEmpty()) {
             builder.setEndpointConfiguration(
                     new 
AwsClientBuilder.EndpointConfiguration(this.getAwsEndpoint(), 
this.getAwsRegion()));
-        }
-        if (!this.getAwsRegion().isEmpty()) {
+        } else if (!this.getAwsRegion().isEmpty()) {
             builder.setRegion(this.getAwsRegion());
         }
         builder.setCredentials(credPlugin.getCredentialProvider());
@@ -198,11 +199,10 @@ public class DynamoDBSourceConfig implements Serializable 
{
     public AmazonDynamoDB buildDynamoDBClient(AwsCredentialProviderPlugin 
credPlugin) {
         AmazonDynamoDBClientBuilder builder = 
AmazonDynamoDBClientBuilder.standard();
 
-        if (!this.getAwsEndpoint().isEmpty()) {
+        if (!this.getDynamoEndpoint().isEmpty()) {
             builder.setEndpointConfiguration(
                     new 
AwsClientBuilder.EndpointConfiguration(this.getDynamoEndpoint(), 
this.getAwsRegion()));
-        }
-        if (!this.getAwsRegion().isEmpty()) {
+        } else if (!this.getAwsRegion().isEmpty()) {
             builder.setRegion(this.getAwsRegion());
         }
         builder.setCredentials(credPlugin.getCredentialProvider());
@@ -212,11 +212,10 @@ public class DynamoDBSourceConfig implements Serializable 
{
     public AmazonCloudWatch buildCloudwatchClient(AwsCredentialProviderPlugin 
credPlugin) {
         AmazonCloudWatchClientBuilder builder = 
AmazonCloudWatchClientBuilder.standard();
 
-        if (!this.getAwsEndpoint().isEmpty()) {
+        if (!this.getCloudwatchEndpoint().isEmpty()) {
             builder.setEndpointConfiguration(
                     new 
AwsClientBuilder.EndpointConfiguration(this.getCloudwatchEndpoint(), 
this.getAwsRegion()));
-        }
-        if (!this.getAwsRegion().isEmpty()) {
+        } else if (!this.getAwsRegion().isEmpty()) {
             builder.setRegion(this.getAwsRegion());
         }
         builder.setCredentials(credPlugin.getCredentialProvider());
diff --git 
a/dynamodb/src/test/java/org/apache/pulsar/io/dynamodb/DynamoDBSourceConfigClientBuilderTest.java
 
b/dynamodb/src/test/java/org/apache/pulsar/io/dynamodb/DynamoDBSourceConfigClientBuilderTest.java
new file mode 100644
index 00000000..b0269fa5
--- /dev/null
+++ 
b/dynamodb/src/test/java/org/apache/pulsar/io/dynamodb/DynamoDBSourceConfigClientBuilderTest.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.io.dynamodb;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import com.amazonaws.AmazonWebServiceClient;
+import com.amazonaws.auth.AWSCredentialsProvider;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import java.lang.reflect.Field;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.pulsar.io.aws.AwsCredentialProviderPlugin;
+import org.apache.pulsar.io.core.SourceContext;
+import org.mockito.Mockito;
+import org.testng.annotations.Test;
+
+/**
+ * Regression tests for client construction when a custom endpoint is 
configured.
+ *
+ * <p>All three builders used to set an {@code EndpointConfiguration} and 
then, in a separate
+ * branch, a region. AWS SDK v1 rejects that pairing with
+ * {@code IllegalStateException: Only one of Region or EndpointConfiguration 
may be set}, and
+ * {@code DynamoDBSource.open()} requires {@code awsRegion}, so every 
configuration supplying an
+ * endpoint failed at construction — the endpoint path could never have worked.
+ *
+ * <p>Two builders also gated on {@code awsEndpoint} while reading {@code 
dynamoEndpoint} /
+ * {@code cloudwatchEndpoint}, so setting {@code awsEndpoint} alone built an 
endpoint
+ * configuration from an empty string. These tests pin both behaviors.
+ */
+public class DynamoDBSourceConfigClientBuilderTest {
+
+    private static final String REGION = "us-east-1";
+    private static final String STREAMS_ENDPOINT = 
"http://localhost:4566/streams";;
+    private static final String DYNAMO_ENDPOINT = 
"http://localhost:4566/dynamo";;
+    private static final String CLOUDWATCH_ENDPOINT = 
"http://localhost:4566/cloudwatch";;
+
+    /** Minimal plugin: the builders only need a credential provider. */
+    private static final AwsCredentialProviderPlugin CREDENTIALS = new 
AwsCredentialProviderPlugin() {
+        @Override
+        public void init(String param) {
+        }
+
+        @Override
+        public AWSCredentialsProvider getCredentialProvider() {
+            return new AWSStaticCredentialsProvider(new 
BasicAWSCredentials("access", "secret"));
+        }
+
+        @Override
+        public void close() {
+        }
+    };
+
+    private DynamoDBSourceConfig config(Map<String, Object> overrides) throws 
Exception {
+        Map<String, Object> map = new HashMap<>();
+        map.put("awsDynamodbStreamArn", 
"arn:aws:dynamodb:us-east-1:1:table/t/stream/1");
+        map.put("awsRegion", REGION);
+        map.put("awsCredentialPluginParam", 
"{\"accessKey\":\"a\",\"secretKey\":\"s\"}");
+        map.putAll(overrides);
+        return DynamoDBSourceConfig.load(map, 
Mockito.mock(SourceContext.class));
+    }
+
+    /** Reads the endpoint the SDK actually resolved for a built client. */
+    private static URI endpointOf(Object client) throws Exception {
+        Field field = 
AmazonWebServiceClient.class.getDeclaredField("endpoint");
+        field.setAccessible(true);
+        return (URI) field.get(client);
+    }
+
+    /**
+     * The bug: an endpoint plus the (mandatory) region made the SDK throw
+     * {@code Only one of Region or EndpointConfiguration may be set}.
+     */
+    @Test
+    public void testBuildersDoNotThrowWhenEndpointAndRegionAreBothSet() throws 
Exception {
+        DynamoDBSourceConfig config = config(Map.of(
+                "awsEndpoint", STREAMS_ENDPOINT,
+                "dynamoEndpoint", DYNAMO_ENDPOINT,
+                "cloudwatchEndpoint", CLOUDWATCH_ENDPOINT));
+
+        // Construction is the assertion: each of these threw 
IllegalStateException before the fix.
+        config.buildDynamoDBStreamsClient(CREDENTIALS);
+        config.buildDynamoDBClient(CREDENTIALS);
+        config.buildCloudwatchClient(CREDENTIALS);
+    }
+
+    /** Each builder must use its own endpoint field, not another client's. */
+    @Test
+    public void testEachBuilderSelectsItsOwnEndpoint() throws Exception {
+        DynamoDBSourceConfig config = config(Map.of(
+                "awsEndpoint", STREAMS_ENDPOINT,
+                "dynamoEndpoint", DYNAMO_ENDPOINT,
+                "cloudwatchEndpoint", CLOUDWATCH_ENDPOINT));
+
+        
assertEquals(endpointOf(config.buildDynamoDBStreamsClient(CREDENTIALS)).toString(),
 STREAMS_ENDPOINT);
+        
assertEquals(endpointOf(config.buildDynamoDBClient(CREDENTIALS)).toString(), 
DYNAMO_ENDPOINT);
+        
assertEquals(endpointOf(config.buildCloudwatchClient(CREDENTIALS)).toString(), 
CLOUDWATCH_ENDPOINT);
+    }
+
+    /**
+     * Setting only {@code awsEndpoint} used to make the DynamoDB and 
CloudWatch builders construct an
+     * endpoint configuration from an empty {@code dynamoEndpoint} / {@code 
cloudwatchEndpoint}.
+     * They must fall back to the region's default endpoint instead.
+     */
+    @Test
+    public void testAwsEndpointAloneDoesNotLeakIntoTheOtherClients() throws 
Exception {
+        DynamoDBSourceConfig config = config(Map.of("awsEndpoint", 
STREAMS_ENDPOINT));
+
+        
assertEquals(endpointOf(config.buildDynamoDBStreamsClient(CREDENTIALS)).toString(),
 STREAMS_ENDPOINT);
+
+        URI dynamoEndpoint = 
endpointOf(config.buildDynamoDBClient(CREDENTIALS));
+        assertTrue(dynamoEndpoint.toString().contains("dynamodb." + REGION),
+                "DynamoDB client should use the regional default endpoint, got 
" + dynamoEndpoint);
+
+        URI cloudwatchEndpoint = 
endpointOf(config.buildCloudwatchClient(CREDENTIALS));
+        assertTrue(cloudwatchEndpoint.toString().contains("monitoring." + 
REGION),
+                "CloudWatch client should use the regional default endpoint, 
got " + cloudwatchEndpoint);
+    }
+
+    /** The common case against real AWS: no endpoint override at all. */
+    @Test
+    public void testRegionOnlyUsesRegionalEndpoints() throws Exception {
+        DynamoDBSourceConfig config = config(Map.of());
+
+        
assertTrue(endpointOf(config.buildDynamoDBStreamsClient(CREDENTIALS)).toString()
+                .contains("streams.dynamodb." + REGION));
+        
assertTrue(endpointOf(config.buildDynamoDBClient(CREDENTIALS)).toString()
+                .contains("dynamodb." + REGION));
+        
assertTrue(endpointOf(config.buildCloudwatchClient(CREDENTIALS)).toString()
+                .contains("monitoring." + REGION));
+    }
+}

Reply via email to