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

dimas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git


The following commit(s) were added to refs/heads/main by this push:
     new e439ff5b0 feat: Add `endpointInternal` to `AwsStorageConfigInfo` 
(#2213)
e439ff5b0 is described below

commit e439ff5b0809220bbc122f321c21423298cf4f2b
Author: Dmitri Bourlatchkov <dmitri.bourlatch...@gmail.com>
AuthorDate: Fri Aug 1 09:15:12 2025 -0400

    feat: Add `endpointInternal` to `AwsStorageConfigInfo` (#2213)
    
    * feat: Add `endpointInternal` to `AwsStorageConfigInfo`
    
    This API change is backward compatible with older clients
    and server using old storage configuration.
    
    * The `endpointInternal` allows Polaris Servers to use a different
      host name (or IP address) for accessing S3 storage than clients.
      This is not a common use case, but may be relevant is more complex
      environments.
    
    * If not set `endpointInternal` defaults to `endpoint`.
    
    * The STS endpoint default changes to `endpointInternal`.
    
    Contributes to #1530
---
 .../apache/polaris/core/entity/CatalogEntity.java  |  3 +-
 .../aws/AwsCredentialsStorageIntegration.java      |  5 ++
 .../storage/aws/AwsStorageConfigurationInfo.java   | 20 ++++--
 .../aws/AwsStorageConfigurationInfoTest.java       | 47 ++++++++++++-
 .../it/QuarkusRestCatalogMinIOSpecialIT.java       | 77 +++++++++++++++-------
 spec/polaris-management-service.yml                | 15 ++++-
 6 files changed, 134 insertions(+), 33 deletions(-)

diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java 
b/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java
index 0de3c3daa..ba8627f06 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java
@@ -277,7 +277,8 @@ public class CatalogEntity extends PolarisEntity implements 
LocationBasedEntity
                     awsConfigModel.getRegion(),
                     awsConfigModel.getEndpoint(),
                     awsConfigModel.getStsEndpoint(),
-                    awsConfigModel.getPathStyleAccess());
+                    awsConfigModel.getPathStyleAccess(),
+                    awsConfigModel.getEndpointInternal());
             awsConfig.validateArn(awsConfigModel.getRoleArn());
             config = awsConfig;
             break;
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java
index f619a9a13..9376d6781 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java
@@ -120,6 +120,11 @@ public class AwsCredentialsStorageIntegration
     if (endpointUri != null) {
       accessConfig.put(StorageAccessProperty.AWS_ENDPOINT, 
endpointUri.toString());
     }
+    URI internalEndpointUri = storageConfig.getInternalEndpointUri();
+    if (internalEndpointUri != null) {
+      accessConfig.putInternalProperty(
+          StorageAccessProperty.AWS_ENDPOINT.getPropertyName(), 
internalEndpointUri.toString());
+    }
 
     if (Boolean.TRUE.equals(storageConfig.getPathStyleAccess())) {
       accessConfig.put(StorageAccessProperty.AWS_PATH_STYLE_ACCESS, 
Boolean.TRUE.toString());
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsStorageConfigurationInfo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsStorageConfigurationInfo.java
index a007c6a71..bd325d103 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsStorageConfigurationInfo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsStorageConfigurationInfo.java
@@ -58,6 +58,10 @@ public class AwsStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo
   @JsonProperty(value = "endpoint")
   private @Nullable String endpoint;
 
+  /** Endpoint URI for internal Polaris calls to S3 API */
+  @JsonProperty(value = "endpointInternal")
+  private @Nullable String endpointInternal;
+
   /** Endpoint URI for STS API calls */
   @JsonProperty(value = "stsEndpoint")
   private @Nullable String stsEndpoint;
@@ -76,7 +80,8 @@ public class AwsStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo
       @JsonProperty(value = "region", required = false) @Nullable String 
region,
       @JsonProperty(value = "endpoint") @Nullable String endpoint,
       @JsonProperty(value = "stsEndpoint") @Nullable String stsEndpoint,
-      @JsonProperty(value = "pathStyleAccess") @Nullable Boolean 
pathStyleAccess) {
+      @JsonProperty(value = "pathStyleAccess") @Nullable Boolean 
pathStyleAccess,
+      @JsonProperty(value = "endpointInternal") @Nullable String 
endpointInternal) {
     super(storageType, allowedLocations);
     this.roleARN = roleARN;
     this.externalId = externalId;
@@ -84,6 +89,7 @@ public class AwsStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo
     this.endpoint = endpoint;
     this.stsEndpoint = stsEndpoint;
     this.pathStyleAccess = pathStyleAccess;
+    this.endpointInternal = endpointInternal;
   }
 
   public AwsStorageConfigurationInfo(
@@ -91,7 +97,7 @@ public class AwsStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo
       @Nonnull List<String> allowedLocations,
       @Nonnull String roleARN,
       @Nullable String region) {
-    this(storageType, allowedLocations, roleARN, null, region, null, null, 
null);
+    this(storageType, allowedLocations, roleARN, null, region, null, null, 
null, null);
   }
 
   public AwsStorageConfigurationInfo(
@@ -100,7 +106,7 @@ public class AwsStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo
       @Nonnull String roleARN,
       @Nullable String externalId,
       @Nullable String region) {
-    this(storageType, allowedLocations, roleARN, externalId, region, null, 
null, null);
+    this(storageType, allowedLocations, roleARN, externalId, region, null, 
null, null, null);
   }
 
   @Override
@@ -160,6 +166,12 @@ public class AwsStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo
     return endpoint == null ? null : URI.create(endpoint);
   }
 
+  @JsonIgnore
+  @Nullable
+  public URI getInternalEndpointUri() {
+    return endpointInternal == null ? getEndpointUri() : 
URI.create(endpointInternal);
+  }
+
   /** Returns a flag indicating whether path-style bucket access should be 
forced in S3 clients. */
   public @Nullable Boolean getPathStyleAccess() {
     return pathStyleAccess;
@@ -174,7 +186,7 @@ public class AwsStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo
   @JsonIgnore
   @Nullable
   public URI getStsEndpointUri() {
-    return stsEndpoint == null ? getEndpointUri() : URI.create(stsEndpoint);
+    return stsEndpoint == null ? getInternalEndpointUri() : 
URI.create(stsEndpoint);
   }
 
   @JsonIgnore
diff --git 
a/polaris-core/src/test/java/org/apache/polaris/core/storage/aws/AwsStorageConfigurationInfoTest.java
 
b/polaris-core/src/test/java/org/apache/polaris/core/storage/aws/AwsStorageConfigurationInfoTest.java
index f33935b4f..8605b8142 100644
--- 
a/polaris-core/src/test/java/org/apache/polaris/core/storage/aws/AwsStorageConfigurationInfoTest.java
+++ 
b/polaris-core/src/test/java/org/apache/polaris/core/storage/aws/AwsStorageConfigurationInfoTest.java
@@ -34,8 +34,13 @@ public class AwsStorageConfigurationInfoTest {
 
   private static AwsStorageConfigurationInfo config(
       String endpoint, String stsEndpoint, Boolean pathStyle) {
+    return config(endpoint, stsEndpoint, pathStyle, null);
+  }
+
+  private static AwsStorageConfigurationInfo config(
+      String endpoint, String stsEndpoint, Boolean pathStyle, String 
internalEndpoint) {
     return new AwsStorageConfigurationInfo(
-        S3, List.of(), "role", null, null, endpoint, stsEndpoint, pathStyle);
+        S3, List.of(), "role", null, null, endpoint, stsEndpoint, pathStyle, 
internalEndpoint);
   }
 
   @Test
@@ -60,6 +65,46 @@ public class AwsStorageConfigurationInfoTest {
             AwsStorageConfigurationInfo::getEndpointUri,
             AwsStorageConfigurationInfo::getStsEndpointUri)
         .containsExactly(URI.create("http://s3.example.com";), 
URI.create("http://sts.example.com";));
+    assertThat(config("http://s3.example.com";, null, false, 
"http://int.example.com";))
+        .extracting(
+            AwsStorageConfigurationInfo::getEndpointUri,
+            AwsStorageConfigurationInfo::getStsEndpointUri,
+            AwsStorageConfigurationInfo::getInternalEndpointUri)
+        .containsExactly(
+            URI.create("http://s3.example.com";),
+            URI.create("http://int.example.com";),
+            URI.create("http://int.example.com";));
+  }
+
+  @Test
+  public void testInternalEndpoint() {
+    assertThat(config(null, null))
+        .extracting(
+            AwsStorageConfigurationInfo::getEndpointUri,
+            AwsStorageConfigurationInfo::getInternalEndpointUri)
+        .containsExactly(null, null);
+    assertThat(config(null, "http://sts.example.com";))
+        .extracting(
+            AwsStorageConfigurationInfo::getEndpointUri,
+            AwsStorageConfigurationInfo::getInternalEndpointUri)
+        .containsExactly(null, null);
+    assertThat(config("http://s3.example.com";, null))
+        .extracting(
+            AwsStorageConfigurationInfo::getEndpointUri,
+            AwsStorageConfigurationInfo::getInternalEndpointUri)
+        .containsExactly(URI.create("http://s3.example.com";), 
URI.create("http://s3.example.com";));
+    assertThat(
+            config(
+                "http://s3.example.com";, "http://sts.example.com";, false, 
"http://int.example.com";))
+        .extracting(
+            AwsStorageConfigurationInfo::getEndpointUri,
+            AwsStorageConfigurationInfo::getInternalEndpointUri)
+        .containsExactly(URI.create("http://s3.example.com";), 
URI.create("http://int.example.com";));
+    assertThat(config(null, "http://sts.example.com";, false, 
"http://int.example.com";))
+        .extracting(
+            AwsStorageConfigurationInfo::getEndpointUri,
+            AwsStorageConfigurationInfo::getInternalEndpointUri)
+        .containsExactly(null, URI.create("http://int.example.com";));
   }
 
   @Test
diff --git 
a/runtime/service/src/intTest/java/org/apache/polaris/service/quarkus/it/QuarkusRestCatalogMinIOSpecialIT.java
 
b/runtime/service/src/intTest/java/org/apache/polaris/service/quarkus/it/QuarkusRestCatalogMinIOSpecialIT.java
index d75cf2d3a..94f7db17f 100644
--- 
a/runtime/service/src/intTest/java/org/apache/polaris/service/quarkus/it/QuarkusRestCatalogMinIOSpecialIT.java
+++ 
b/runtime/service/src/intTest/java/org/apache/polaris/service/quarkus/it/QuarkusRestCatalogMinIOSpecialIT.java
@@ -69,6 +69,7 @@ import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInfo;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.params.ParameterizedTest;
@@ -154,6 +155,14 @@ public class QuarkusRestCatalogMinIOSpecialIT {
 
   private RESTCatalog createCatalog(
       Optional<String> endpoint, Optional<String> stsEndpoint, boolean 
pathStyleAccess) {
+    return createCatalog(endpoint, stsEndpoint, pathStyleAccess, 
Optional.empty());
+  }
+
+  private RESTCatalog createCatalog(
+      Optional<String> endpoint,
+      Optional<String> stsEndpoint,
+      boolean pathStyleAccess,
+      Optional<String> endpointInternal) {
     AwsStorageConfigInfo.Builder storageConfig =
         AwsStorageConfigInfo.builder()
             .setRoleArn("arn:aws:iam::123456789012:role/polaris-test")
@@ -165,6 +174,7 @@ public class QuarkusRestCatalogMinIOSpecialIT {
 
     endpoint.ifPresent(storageConfig::setEndpoint);
     stsEndpoint.ifPresent(storageConfig::setStsEndpoint);
+    endpointInternal.ifPresent(storageConfig::setEndpointInternal);
 
     CatalogProperties.Builder catalogProps =
         CatalogProperties.builder(storageBase.toASCIIString() + "/" + 
catalogName);
@@ -204,39 +214,56 @@ public class QuarkusRestCatalogMinIOSpecialIT {
   public void testCreateTable(boolean pathStyle) throws IOException {
     try (RESTCatalog restCatalog =
         createCatalog(Optional.of(endpoint), Optional.empty(), pathStyle)) {
-      catalogApi.createNamespace(catalogName, "test-ns");
-      TableIdentifier id = TableIdentifier.of("test-ns", "t1");
-      Table table = restCatalog.createTable(id, SCHEMA);
-      assertThat(table).isNotNull();
-      assertThat(restCatalog.tableExists(id)).isTrue();
-
-      TableOperations ops = ((HasTableOperations) table).operations();
-      URI location = URI.create(ops.current().metadataFileLocation());
-
-      GetObjectResponse response =
-          s3Client
-              .getObject(
-                  GetObjectRequest.builder()
-                      .bucket(location.getAuthority())
-                      .key(location.getPath().substring(1)) // drop leading 
slash
-                      .build())
-              .response();
-      assertThat(response.contentLength()).isGreaterThan(0);
-
-      LoadTableResponse loadTableResponse =
-          catalogApi.loadTableWithAccessDelegation(catalogName, id, "ALL");
-      assertThat(loadTableResponse.config()).containsKey("s3.endpoint");
-
+      LoadTableResponse loadTableResponse = doTestCreateTable(restCatalog);
       if (pathStyle) {
         assertThat(loadTableResponse.config())
             .containsEntry("s3.path-style-access", Boolean.TRUE.toString());
       }
+    }
+  }
 
-      restCatalog.dropTable(id);
-      assertThat(restCatalog.tableExists(id)).isFalse();
+  @Test
+  public void testInternalEndpoints() throws IOException {
+    try (RESTCatalog restCatalog =
+        createCatalog(
+            Optional.of("http://s3.example.com";),
+            Optional.of(endpoint),
+            false,
+            Optional.of(endpoint))) {
+      LoadTableResponse loadTableResponse = doTestCreateTable(restCatalog);
+      assertThat(loadTableResponse.config()).containsEntry("s3.endpoint", 
"http://s3.example.com";);
     }
   }
 
+  public LoadTableResponse doTestCreateTable(RESTCatalog restCatalog) throws 
IOException {
+    catalogApi.createNamespace(catalogName, "test-ns");
+    TableIdentifier id = TableIdentifier.of("test-ns", "t1");
+    Table table = restCatalog.createTable(id, SCHEMA);
+    assertThat(table).isNotNull();
+    assertThat(restCatalog.tableExists(id)).isTrue();
+
+    TableOperations ops = ((HasTableOperations) table).operations();
+    URI location = URI.create(ops.current().metadataFileLocation());
+
+    GetObjectResponse response =
+        s3Client
+            .getObject(
+                GetObjectRequest.builder()
+                    .bucket(location.getAuthority())
+                    .key(location.getPath().substring(1)) // drop leading slash
+                    .build())
+            .response();
+    assertThat(response.contentLength()).isGreaterThan(0);
+
+    LoadTableResponse loadTableResponse =
+        catalogApi.loadTableWithAccessDelegation(catalogName, id, "ALL");
+    assertThat(loadTableResponse.config()).containsKey("s3.endpoint");
+
+    restCatalog.dropTable(id);
+    assertThat(restCatalog.tableExists(id)).isFalse();
+    return loadTableResponse;
+  }
+
   @ParameterizedTest
   @ValueSource(booleans = {true, false})
   public void testAppendFiles(boolean pathStyle) throws IOException {
diff --git a/spec/polaris-management-service.yml 
b/spec/polaris-management-service.yml
index 5a2461ebd..a7398bd39 100644
--- a/spec/polaris-management-service.yml
+++ b/spec/polaris-management-service.yml
@@ -1050,12 +1050,23 @@ components:
               example: "us-east-2"
             endpoint:
               type: string
-              description: endpoint for S3 requests (optional)
+              description: >-
+                endpoint for S3 requests (optional). Clients always see this 
value (if it is set). Polaris Servers
+                may be configured to use a different endpoint URI via the 
`endpointInternal` property.
               example: "https://s3.example.com:1234";
             stsEndpoint:
               type: string
-              description: endpoint for STS requests (optional). If not set, 
defaults to 'endpoint'.
+              description: >-
+                endpoint for STS requests made by the Polaris Server 
(optional). If not set, defaults to
+                'endpointInternal' (which in turn defaults to `endpoint`).
               example: "https://sts.example.com:1234";
+            endpointInternal:
+              type: string
+              description: >-
+                endpoint for S3 requests made by the Polaris Server 
(optional). If set, Polaris Service will use
+                this value instead of `endpoint`. If not set, defaults to 
`endpoint`. Iceberg REST API clients never
+                see this value.
+              example: "https://s3.internal.example.com:1234";
             pathStyleAccess:
               type: boolean
               description: >- 

Reply via email to