Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ at locations that better optimize for object storage.

### Changes

- Polaris Management API clients must be prepared to deal with new attributes in `AwsStorageConfigInfo` objects.

### Deprecations

* The property `polaris.active-roles-provider.type` is deprecated in favor of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public void testJsonFormat() throws JsonProcessingException {
+ "\"properties\":{\"default-base-location\":\"s3://test/\"},"
+ "\"storageConfigInfo\":{"
+ "\"roleArn\":\"arn:aws:iam::123456789012:role/test-role\","
+ "\"pathStyleAccess\":false,"
+ "\"storageType\":\"S3\","
+ "\"allowedLocations\":[]"
+ "}}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,27 @@ public void dropTable(String catalog, TableIdentifier id) {
}

public LoadTableResponse loadTable(String catalog, TableIdentifier id, String snapshots) {
return loadTable(catalog, id, snapshots, Map.of());
}

public LoadTableResponse loadTableWithAccessDelegation(
String catalog, TableIdentifier id, String snapshots) {
return loadTable(
catalog, id, snapshots, Map.of("X-Iceberg-Access-Delegation", "vended-credentials"));
}

public LoadTableResponse loadTable(
String catalog, TableIdentifier id, String snapshots, Map<String, String> headers) {
HashMap<String, String> allHeaders = new HashMap<>(defaultHeaders());
allHeaders.putAll(headers);

String ns = RESTUtil.encodeNamespace(id.namespace());
try (Response res =
request(
"v1/{cat}/namespaces/" + ns + "/tables/{table}",
Map.of("cat", catalog, "table", id.name()),
snapshots == null ? Map.of() : Map.of("snapshots", snapshots))
snapshots == null ? Map.of() : Map.of("snapshots", snapshots),
allHeaders)
.get()) {
if (res.getStatus() == Response.Status.OK.getStatusCode()) {
return res.readEntity(LoadTableResponse.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,18 @@ public Invocation.Builder request(String path, Map<String, String> templateValue
return request(path, templateValues, Map.of());
}

public Invocation.Builder request(
String path, Map<String, String> templateValues, Map<String, String> queryParams) {
protected Map<String, String> defaultHeaders() {
Map<String, String> headers = new HashMap<>();
headers.put(endpoints.realmHeaderName(), endpoints.realmId());
if (authToken != null) {
headers.put("Authorization", "Bearer " + authToken);
}
return request(path, templateValues, queryParams, headers);
return headers;
}

public Invocation.Builder request(
String path, Map<String, String> templateValues, Map<String, String> queryParams) {
return request(path, templateValues, queryParams, defaultHeaders());
}

public Invocation.Builder request(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ private StorageConfigInfo getStorageInfo(Map<String, String> internalProperties)
.setStorageType(StorageConfigInfo.StorageTypeEnum.S3)
.setAllowedLocations(awsConfig.getAllowedLocations())
.setRegion(awsConfig.getRegion())
.setEndpoint(awsConfig.getEndpoint())
.setStsEndpoint(awsConfig.getStsEndpoint())
.setPathStyleAccess(awsConfig.getPathStyleAccess())
.build();
}
if (configInfo instanceof AzureStorageConfigurationInfo) {
Expand Down Expand Up @@ -275,7 +278,8 @@ public Builder setStorageConfigurationInfo(
awsConfigModel.getExternalId(),
awsConfigModel.getRegion(),
awsConfigModel.getEndpoint(),
awsConfigModel.getStsEndpoint());
awsConfigModel.getStsEndpoint(),
awsConfigModel.getPathStyleAccess());
awsConfig.validateArn(awsConfigModel.getRoleArn());
config = awsConfig;
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ public EnumMap<StorageAccessProperty, String> getSubscopedCreds(
credentialMap.put(StorageAccessProperty.AWS_ENDPOINT, endpointUri.toString());
}

if (Boolean.TRUE.equals(storageConfig.getPathStyleAccess())) {
credentialMap.put(StorageAccessProperty.AWS_PATH_STYLE_ACCESS, Boolean.TRUE.toString());
}

if (storageConfig.getAwsPartition().equals("aws-us-gov")
&& credentialMap.get(StorageAccessProperty.CLIENT_REGION) == null) {
throw new IllegalArgumentException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ public class AwsStorageConfigurationInfo extends PolarisStorageConfigurationInfo
@JsonProperty(value = "stsEndpoint")
private @Nullable String stsEndpoint;

/** A flag indicating whether path-style bucket access should be forced in S3 clients. */
@JsonProperty(value = "pathStyleAccess")
private Boolean pathStyleAccess;

@JsonCreator
public AwsStorageConfigurationInfo(
@JsonProperty(value = "storageType", required = true) @Nonnull StorageType storageType,
Expand All @@ -71,21 +75,23 @@ public AwsStorageConfigurationInfo(
@JsonProperty(value = "externalId") @Nullable String externalId,
@JsonProperty(value = "region", required = false) @Nullable String region,
@JsonProperty(value = "endpoint") @Nullable String endpoint,
@JsonProperty(value = "stsEndpoint") @Nullable String stsEndpoint) {
@JsonProperty(value = "stsEndpoint") @Nullable String stsEndpoint,
@JsonProperty(value = "pathStyleAccess") @Nullable Boolean pathStyleAccess) {
super(storageType, allowedLocations);
this.roleARN = roleARN;
this.externalId = externalId;
this.region = region;
this.endpoint = endpoint;
this.stsEndpoint = stsEndpoint;
this.pathStyleAccess = pathStyleAccess;
}

public AwsStorageConfigurationInfo(
@Nonnull StorageType storageType,
@Nonnull List<String> allowedLocations,
@Nonnull String roleARN,
@Nullable String region) {
this(storageType, allowedLocations, roleARN, null, region, null, null);
this(storageType, allowedLocations, roleARN, null, region, null, null, null);
}

public AwsStorageConfigurationInfo(
Expand All @@ -94,7 +100,7 @@ public AwsStorageConfigurationInfo(
@Nonnull String roleARN,
@Nullable String externalId,
@Nullable String region) {
this(storageType, allowedLocations, roleARN, externalId, region, null, null);
this(storageType, allowedLocations, roleARN, externalId, region, null, null, null);
}

@Override
Expand Down Expand Up @@ -143,12 +149,27 @@ public void setRegion(@Nullable String region) {
this.region = region;
}

@Nullable
public String getEndpoint() {
return endpoint;
}

@JsonIgnore
@Nullable
public URI getEndpointUri() {
return endpoint == null ? null : URI.create(endpoint);
}

/** Returns a flag indicating whether path-style bucket access should be forced in S3 clients. */
public @Nullable Boolean getPathStyleAccess() {
return pathStyleAccess;
}

@Nullable
public String getStsEndpoint() {
return stsEndpoint;
}

/** Returns the STS endpoint if set, defaulting to {@link #getEndpointUri()} otherwise. */
@JsonIgnore
@Nullable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@
public class AwsStorageConfigurationInfoTest {

private static AwsStorageConfigurationInfo config(String endpoint, String stsEndpoint) {
return config(endpoint, stsEndpoint, false);
}

private static AwsStorageConfigurationInfo config(
String endpoint, String stsEndpoint, Boolean pathStyle) {
return new AwsStorageConfigurationInfo(
S3, List.of(), "role", null, null, endpoint, stsEndpoint);
S3, List.of(), "role", null, null, endpoint, stsEndpoint, pathStyle);
}

@Test
Expand All @@ -56,4 +61,11 @@ public void testStsEndpoint() {
AwsStorageConfigurationInfo::getStsEndpointUri)
.containsExactly(URI.create("http://s3.example.com"), URI.create("http://sts.example.com"));
}

@Test
public void testPathStyleAccess() {
assertThat(config(null, null, null).getPathStyleAccess()).isNull();
assertThat(config(null, null, false).getPathStyleAccess()).isFalse();
assertThat(config(null, null, true).getPathStyleAccess()).isTrue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import org.apache.iceberg.io.PositionOutputStream;
import org.apache.iceberg.rest.RESTCatalog;
import org.apache.iceberg.rest.auth.OAuth2Properties;
import org.apache.iceberg.rest.responses.LoadTableResponse;
import org.apache.iceberg.types.Types;
import org.apache.polaris.core.admin.model.AwsStorageConfigInfo;
import org.apache.polaris.core.admin.model.Catalog;
Expand All @@ -68,9 +69,10 @@
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;
import org.junit.jupiter.params.provider.ValueSource;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
Expand Down Expand Up @@ -145,13 +147,15 @@ public void before(TestInfo testInfo) {
catalogName = client.newEntityName(testInfo.getTestMethod().orElseThrow().getName());
}

private RESTCatalog createCatalog(Optional<String> endpoint, Optional<String> stsEndpoint) {
private RESTCatalog createCatalog(
Optional<String> endpoint, Optional<String> stsEndpoint, boolean pathStyleAccess) {
AwsStorageConfigInfo.Builder storageConfig =
AwsStorageConfigInfo.builder()
.setRoleArn("arn:aws:iam::123456789012:role/polaris-test")
.setExternalId("externalId123")
.setUserArn("arn:aws:iam::123456789012:user/polaris-test")
.setStorageType(StorageConfigInfo.StorageTypeEnum.S3)
.setPathStyleAccess(pathStyleAccess)
.setAllowedLocations(List.of(storageBase.toString()));

endpoint.ifPresent(storageConfig::setEndpoint);
Expand Down Expand Up @@ -190,9 +194,11 @@ public void cleanUp() {
client.cleanUp(adminCredentials);
}

@Test
public void testCreateTable() throws IOException {
try (RESTCatalog restCatalog = createCatalog(Optional.of(endpoint), Optional.empty())) {
@ParameterizedTest
@ValueSource(booleans = {true, false})
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);
Expand All @@ -212,14 +218,25 @@ public void testCreateTable() throws IOException {
.response();
assertThat(response.contentLength()).isGreaterThan(0);

LoadTableResponse loadTableResponse =
catalogApi.loadTableWithAccessDelegation(catalogName, id, "ALL");
assertThat(loadTableResponse.config()).containsKey("s3.endpoint");

if (pathStyle) {
assertThat(loadTableResponse.config())
.containsEntry("s3.path-style-access", Boolean.TRUE.toString());
}

restCatalog.dropTable(id);
assertThat(restCatalog.tableExists(id)).isFalse();
}
}

@Test
public void testAppendFiles() throws IOException {
try (RESTCatalog restCatalog = createCatalog(Optional.of(endpoint), Optional.of(endpoint))) {
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testAppendFiles(boolean pathStyle) throws IOException {
try (RESTCatalog restCatalog =
createCatalog(Optional.of(endpoint), Optional.of(endpoint), pathStyle)) {
catalogApi.createNamespace(catalogName, "test-ns");
TableIdentifier id = TableIdentifier.of("test-ns", "t1");
Table table = restCatalog.createTable(id, SCHEMA);
Expand All @@ -228,7 +245,11 @@ public void testAppendFiles() throws IOException {
@SuppressWarnings("resource")
FileIO io = table.io();

URI loc = URI.create(table.locationProvider().newDataLocation("test-file1.txt"));
URI loc =
URI.create(
table
.locationProvider()
.newDataLocation(String.format("test-file-%s.txt", pathStyle)));
OutputFile f1 = io.newOutputFile(loc.toString());
try (PositionOutputStream os = f1.create()) {
os.write("Hello World".getBytes(UTF_8));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
*/
package org.apache.polaris.service.quarkus.entity;

import static org.assertj.core.api.Assertions.assertThat;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import java.util.stream.Stream;
import org.apache.polaris.core.PolarisCallContext;
import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
import org.apache.polaris.core.admin.model.AwsStorageConfigInfo;
Expand All @@ -37,9 +42,12 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;

public class CatalogEntityTest {
private static final ObjectMapper MAPPER = new ObjectMapper();

private CallContext callContext;

Expand Down Expand Up @@ -286,7 +294,7 @@ public void testCatalogTypeDefaultsToInternal() {
.build();

Catalog catalog = catalogEntity.asCatalog();
Assertions.assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.INTERNAL);
assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.INTERNAL);
}

@Test
Expand All @@ -309,7 +317,7 @@ public void testCatalogTypeExternalPreserved() {
.build();

Catalog catalog = catalogEntity.asCatalog();
Assertions.assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.EXTERNAL);
assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.EXTERNAL);
}

@Test
Expand All @@ -332,6 +340,60 @@ public void testCatalogTypeInternalExplicitlySet() {
.build();

Catalog catalog = catalogEntity.asCatalog();
Assertions.assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.INTERNAL);
assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.INTERNAL);
}

@Test
public void testAwsConfigJsonPropertiesPresence() throws JsonProcessingException {
AwsStorageConfigInfo.Builder b =
AwsStorageConfigInfo.builder()
.setStorageType(StorageConfigInfo.StorageTypeEnum.S3)
.setRoleArn("arn:aws:iam::012345678901:role/test-role");
assertThat(MAPPER.writeValueAsString(b.build())).contains("roleArn");
assertThat(MAPPER.writeValueAsString(b.build())).doesNotContain("endpoint");
assertThat(MAPPER.writeValueAsString(b.build())).doesNotContain("stsEndpoint");

b.setEndpoint("http://s3.example.com");
b.setStsEndpoint("http://sts.example.com");
b.setPathStyleAccess(false);
assertThat(MAPPER.writeValueAsString(b.build())).contains("roleArn");
assertThat(MAPPER.writeValueAsString(b.build())).contains("endpoint");
assertThat(MAPPER.writeValueAsString(b.build())).contains("stsEndpoint");
assertThat(MAPPER.writeValueAsString(b.build())).contains("pathStyleAccess");
}

@ParameterizedTest
@MethodSource
public void testAwsConfigRoundTrip(AwsStorageConfigInfo config) throws JsonProcessingException {
String configStr = MAPPER.writeValueAsString(config);
CatalogEntity catalogEntity =
new CatalogEntity.Builder()
.setName("testAwsConfigRoundTrip")
.setDefaultBaseLocation(config.getAllowedLocations().getFirst())
.setCatalogType(Catalog.TypeEnum.INTERNAL.name())
.setStorageConfigurationInfo(
callContext,
MAPPER.readValue(configStr, StorageConfigInfo.class),
config.getAllowedLocations().getFirst())
.build();

Catalog catalog = catalogEntity.asCatalog();
assertThat(catalog.getStorageConfigInfo()).isEqualTo(config);
assertThat(MAPPER.writeValueAsString(catalog.getStorageConfigInfo())).isEqualTo(configStr);
}

public static Stream<Arguments> testAwsConfigRoundTrip() {
AwsStorageConfigInfo.Builder b =
AwsStorageConfigInfo.builder()
.setStorageType(StorageConfigInfo.StorageTypeEnum.S3)
.setAllowedLocations(List.of("s3://example.com"))
.setRoleArn("arn:aws:iam::012345678901:role/test-role");
return Stream.of(
Arguments.of(b.build()),
Arguments.of(b.setExternalId("ex1").build()),
Arguments.of(b.setRegion("us-west-2").build()),
Arguments.of(b.setEndpoint("http://s3.example.com:1234").build()),
Arguments.of(b.setStsEndpoint("http://sts.example.com:1234").build()),
Arguments.of(b.setPathStyleAccess(true).build()));
}
}
Loading
Loading