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: >-