Skip to content

Commit 6bf40a9

Browse files
committed
Add support for S3 request signing
Fixes #32.
1 parent 83be3b1 commit 6bf40a9

File tree

50 files changed

+2010
-280
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2010
-280
lines changed

LICENSE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ License: https://www.apache.org/licenses/LICENSE-2.0
216216
This product includes code from Apache Iceberg.
217217

218218
* spec/iceberg-rest-catalog-open-api.yaml
219+
* spec/iceberg-s3-signer-open-api.yaml
219220
* spec/polaris-catalog-apis/oauth-tokens-api.yaml
220221
* integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java
221222
* runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java

api/iceberg-service/build.gradle.kts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ dependencies {
3131
implementation(platform(libs.iceberg.bom))
3232
implementation("org.apache.iceberg:iceberg-api")
3333
implementation("org.apache.iceberg:iceberg-core")
34+
implementation("org.apache.iceberg:iceberg-aws")
3435

3536
implementation(libs.jakarta.annotation.api)
3637
implementation(libs.jakarta.inject.api)
@@ -49,6 +50,9 @@ dependencies {
4950
implementation("com.fasterxml.jackson.core:jackson-databind")
5051

5152
compileOnly(libs.microprofile.fault.tolerance.api)
53+
54+
compileOnly(project(":polaris-immutables"))
55+
annotationProcessor(project(":polaris-immutables", configuration = "processor"))
5256
}
5357

5458
val rootDir = rootProject.layout.projectDirectory
@@ -70,7 +74,7 @@ openApiGenerate {
7074
ignoreFileOverride.set(provider { rootDir.file(".openapi-generator-ignore").asFile.absolutePath })
7175
templateDir.set(provider { templatesDir.asFile.absolutePath })
7276
removeOperationIdPrefix.set(true)
73-
globalProperties.put("apis", "CatalogApi,ConfigurationApi,OAuth2Api")
77+
globalProperties.put("apis", "CatalogApi,ConfigurationApi,OAuth2Api,S3SignerApi")
7478
globalProperties.put("models", "false")
7579
globalProperties.put("apiDocs", "false")
7680
globalProperties.put("modelTests", "false")
@@ -112,6 +116,10 @@ openApiGenerate {
112116
"IcebergErrorResponse" to "org.apache.iceberg.rest.responses.ErrorResponse",
113117
"OAuthError" to "org.apache.iceberg.rest.responses.ErrorResponse",
114118

119+
// S3 Signing
120+
"S3SignRequest" to "org.apache.polaris.service.types.S3SignRequest",
121+
"SignS3Request200Response" to "org.apache.polaris.service.types.S3SignResponse",
122+
115123
// Custom types defined below
116124
"CommitViewRequest" to "org.apache.polaris.service.types.CommitViewRequest",
117125
"TokenType" to "org.apache.polaris.service.types.TokenType",
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.polaris.service.types;
21+
22+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
23+
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
24+
import jakarta.annotation.Nullable;
25+
import java.net.URI;
26+
import java.util.List;
27+
import java.util.Map;
28+
import org.apache.iceberg.rest.RESTRequest;
29+
import org.apache.polaris.immutables.PolarisImmutable;
30+
import org.immutables.value.Value;
31+
32+
/**
33+
* Request for S3 signing requests.
34+
*
35+
* <p>Copy of {@link org.apache.iceberg.aws.s3.signer.S3SignRequest}, because the original does not
36+
* have Jackson annotations.
37+
*/
38+
@PolarisImmutable
39+
@JsonDeserialize(as = ImmutableS3SignRequest.class)
40+
@JsonSerialize(as = ImmutableS3SignRequest.class)
41+
public interface S3SignRequest extends RESTRequest {
42+
43+
String region();
44+
45+
String method();
46+
47+
URI uri();
48+
49+
Map<String, List<String>> headers();
50+
51+
// unused by Polaris, but required by Iceberg
52+
Map<String, String> properties();
53+
54+
@Value.Default
55+
@Nullable
56+
default String body() {
57+
return null;
58+
}
59+
60+
@Override
61+
default void validate() {}
62+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.polaris.service.types;
21+
22+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
23+
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
24+
import java.net.URI;
25+
import java.util.List;
26+
import java.util.Map;
27+
import org.apache.iceberg.rest.RESTResponse;
28+
import org.apache.polaris.immutables.PolarisImmutable;
29+
30+
/**
31+
* Response for S3 signing requests.
32+
*
33+
* <p>Copy of {@link org.apache.iceberg.aws.s3.signer.S3SignResponse}, because the original does not
34+
* have Jackson annotations.
35+
*/
36+
@PolarisImmutable
37+
@JsonDeserialize(as = ImmutableS3SignResponse.class)
38+
@JsonSerialize(as = ImmutableS3SignResponse.class)
39+
public interface S3SignResponse extends RESTResponse {
40+
41+
URI uri();
42+
43+
Map<String, List<String>> headers();
44+
45+
@Override
46+
default void validate() {}
47+
}

integration-tests/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies {
3333
implementation(platform(libs.iceberg.bom))
3434
implementation("org.apache.iceberg:iceberg-api")
3535
implementation("org.apache.iceberg:iceberg-core")
36+
implementation("org.apache.iceberg:iceberg-aws")
3637

3738
implementation("org.apache.iceberg:iceberg-api:${libs.versions.iceberg.get()}:tests")
3839
implementation("org.apache.iceberg:iceberg-core:${libs.versions.iceberg.get()}:tests")
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.polaris.service.it.env;
21+
22+
import java.lang.annotation.ElementType;
23+
import java.lang.annotation.Inherited;
24+
import java.lang.annotation.Retention;
25+
import java.lang.annotation.RetentionPolicy;
26+
import java.lang.annotation.Target;
27+
import org.apache.polaris.core.admin.model.Catalog;
28+
29+
/**
30+
* Annotation to configure the catalog type and properties for integration tests.
31+
*
32+
* <p>This is a server-side setting; it is used to specify the Polaris Catalog type (e.g., INTERNAL,
33+
* EXTERNAL) and any additional properties required for the catalog configuration.
34+
*/
35+
@Retention(RetentionPolicy.RUNTIME)
36+
@Target({ElementType.TYPE, ElementType.METHOD})
37+
@Inherited
38+
public @interface CatalogConfig {
39+
40+
/** The type of the catalog. Defaults to INTERNAL. */
41+
Catalog.TypeEnum value() default Catalog.TypeEnum.INTERNAL;
42+
43+
/** Additional properties for the catalog configuration. */
44+
String[] properties() default {};
45+
}

integration-tests/src/main/java/org/apache/polaris/service/it/env/IcebergHelper.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import com.google.common.collect.ImmutableMap;
2222
import java.util.Map;
23+
import org.apache.iceberg.CatalogProperties;
2324
import org.apache.iceberg.rest.RESTCatalog;
2425
import org.apache.iceberg.rest.auth.OAuth2Properties;
2526
import org.apache.polaris.core.admin.model.PrincipalWithCredentials;
@@ -38,12 +39,8 @@ public static RESTCatalog restCatalog(
3839

3940
ImmutableMap.Builder<String, String> propertiesBuilder =
4041
ImmutableMap.<String, String>builder()
41-
.put(
42-
org.apache.iceberg.CatalogProperties.URI, endpoints.catalogApiEndpoint().toString())
42+
.put(CatalogProperties.URI, endpoints.catalogApiEndpoint().toString())
4343
.put(OAuth2Properties.TOKEN, authToken)
44-
.put(
45-
org.apache.iceberg.CatalogProperties.FILE_IO_IMPL,
46-
"org.apache.iceberg.inmemory.InMemoryFileIO")
4744
.put("warehouse", catalog)
4845
.put("header." + endpoints.realmHeaderName(), endpoints.realmId())
4946
.putAll(extraProperties);

integration-tests/src/main/java/org/apache/polaris/service/it/env/IntegrationTestsHelper.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,17 @@
1919
package org.apache.polaris.service.it.env;
2020

2121
import com.google.common.annotations.VisibleForTesting;
22+
import com.google.common.collect.ImmutableMap;
23+
import java.lang.annotation.Annotation;
24+
import java.lang.reflect.Method;
2225
import java.net.URI;
2326
import java.nio.file.Path;
27+
import java.util.Arrays;
28+
import java.util.Map;
29+
import java.util.Optional;
2430
import java.util.function.Function;
31+
import java.util.stream.Stream;
32+
import org.junit.jupiter.api.TestInfo;
2533

2634
public final class IntegrationTestsHelper {
2735

@@ -54,4 +62,60 @@ static URI getTemporaryDirectory(Function<String, String> getenv, Path defaultLo
5462
envVar = envVar.startsWith("/") ? "file://" + envVar : envVar;
5563
return URI.create(envVar + "/").normalize();
5664
}
65+
66+
/**
67+
* Extract a value from the annotated elements of the test method and class.
68+
*
69+
* <p>This method looks for annotations of the specified type on both the test method and the test
70+
* class, extracts the value using the provided extractor function, and returns it. If the value
71+
* is not present in either the method or class annotations, the provided default value is
72+
* returned.
73+
*/
74+
public static <A extends Annotation, T> T extractFromAnnotatedElements(
75+
TestInfo testInfo, Class<A> annotationClass, Function<A, T> extractor, T def) {
76+
Method method = testInfo.getTestMethod().orElseThrow();
77+
Optional<T> methodCatalogConfig =
78+
Optional.ofNullable(method.getAnnotation(annotationClass)).map(extractor);
79+
if (methodCatalogConfig.isPresent()) {
80+
return methodCatalogConfig.get();
81+
}
82+
Optional<T> classCatalogConfig =
83+
testInfo.getTestClass().map(clz -> clz.getAnnotation(annotationClass)).map(extractor);
84+
return classCatalogConfig.orElse(def);
85+
}
86+
87+
/**
88+
* Collect properties from annotated elements in the test method and class.
89+
*
90+
* <p>This method looks for annotations of the specified type on both the test method and the test
91+
* class, extracts properties using the provided extractor function, and combines them into a map.
92+
* If a property appears in both the method and class annotations, the value from the method
93+
* annotation will be used.
94+
*/
95+
public static <A extends Annotation> Map<String, String> mergeFromAnnotatedElements(
96+
TestInfo testInfo,
97+
Class<A> annotationClass,
98+
Function<A, String[]> propertiesExtractor,
99+
Map<String, String> defaults) {
100+
Method method = testInfo.getTestMethod().orElseThrow();
101+
Optional<A> methodCatalogConfig = Optional.ofNullable(method.getAnnotation(annotationClass));
102+
Optional<A> classCatalogConfig =
103+
testInfo.getTestClass().map(clz -> clz.getAnnotation(annotationClass));
104+
String[] methodProperties = methodCatalogConfig.map(propertiesExtractor).orElse(new String[0]);
105+
String[] classProperties = classCatalogConfig.map(propertiesExtractor).orElse(new String[0]);
106+
String[] properties =
107+
Stream.concat(Arrays.stream(methodProperties), Arrays.stream(classProperties))
108+
.toArray(String[]::new);
109+
ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
110+
builder.putAll(defaults);
111+
if (properties.length % 2 != 0) {
112+
throw new IllegalArgumentException(
113+
"Properties must be in key-value pairs, but found an odd number of elements: "
114+
+ Arrays.toString(properties));
115+
}
116+
for (int i = 0; i < properties.length; i += 2) {
117+
builder.put(properties[i], properties[i + 1]);
118+
}
119+
return builder.buildKeepingLast();
120+
}
57121
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.polaris.service.it.env;
21+
22+
import java.lang.annotation.ElementType;
23+
import java.lang.annotation.Inherited;
24+
import java.lang.annotation.Retention;
25+
import java.lang.annotation.RetentionPolicy;
26+
import java.lang.annotation.Target;
27+
28+
/**
29+
* Annotation to configure the REST catalog for integration tests.
30+
*
31+
* <p>This is a client-side setting; it is used to configure the client-side REST catalog that is
32+
* used in test code to connect to the Polaris REST API and to the storage layer.
33+
*/
34+
@Retention(RetentionPolicy.RUNTIME)
35+
@Target({ElementType.TYPE, ElementType.METHOD})
36+
@Inherited
37+
public @interface RestCatalogConfig {
38+
String[] value() default {};
39+
}

0 commit comments

Comments
 (0)