diff --git a/pom.xml b/pom.xml
index 9d986b41b05f..e242b179ee5c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -74,6 +74,7 @@
3.3.0
2.6.0
2.2.0
+ 2.11.0
org.openjdk.jmh
@@ -581,6 +594,40 @@
presto-jmx
test
+
+
+ org.apache.iceberg
+ iceberg-core
+ 1.5.0
+ tests
+ test
+
+
+ org.apache.avro
+ avro
+
+
+ org.apache.parquet
+ parquet-column
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.slf4j
+ slf4j-jdk14
+
+
+ io.airlift
+ aircompressor
+
+
+ org.roaringbitmap
+ RoaringBitmap
+
+
+
@@ -590,10 +637,15 @@
org.apache.maven.plugins
maven-dependency-plugin
-
+
org.glassfish.jersey.core:jersey-common:jar
org.eclipse.jetty:jetty-server:jar
+
+ com.facebook.airlift:http-server:jar
+ com.facebook.airlift:node:jar
+ javax.servlet:javax.servlet-api:jar
+ org.apache.httpcomponents.core5:httpcore5:jar
diff --git a/presto-iceberg/src/main/java/com/facebook/presto/iceberg/CatalogType.java b/presto-iceberg/src/main/java/com/facebook/presto/iceberg/CatalogType.java
index 9035d91ad906..c7a54e829cd6 100644
--- a/presto-iceberg/src/main/java/com/facebook/presto/iceberg/CatalogType.java
+++ b/presto-iceberg/src/main/java/com/facebook/presto/iceberg/CatalogType.java
@@ -16,13 +16,14 @@
import org.apache.iceberg.hadoop.HadoopCatalog;
import org.apache.iceberg.hive.HiveCatalog;
import org.apache.iceberg.nessie.NessieCatalog;
+import org.apache.iceberg.rest.RESTCatalog;
public enum CatalogType
{
HADOOP(HadoopCatalog.class.getName()),
HIVE(HiveCatalog.class.getName()),
NESSIE(NessieCatalog.class.getName()),
-
+ REST(RESTCatalog.class.getName())
/**/;
private final String catalogImpl;
diff --git a/presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergCommonModule.java b/presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergCommonModule.java
index 6d1476433e5f..39dbcbc01a34 100644
--- a/presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergCommonModule.java
+++ b/presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergCommonModule.java
@@ -39,6 +39,7 @@
import com.facebook.presto.hive.gcs.HiveGcsConfigurationInitializer;
import com.facebook.presto.iceberg.nessie.NessieConfig;
import com.facebook.presto.iceberg.optimizer.IcebergPlanOptimizerProvider;
+import com.facebook.presto.iceberg.rest.IcebergRestConfig;
import com.facebook.presto.iceberg.procedure.ExpireSnapshotsProcedure;
import com.facebook.presto.iceberg.procedure.RegisterTableProcedure;
import com.facebook.presto.iceberg.procedure.RollbackToSnapshotProcedure;
@@ -128,6 +129,7 @@ public void setup(Binder binder)
configBinder(binder).bindConfig(IcebergConfig.class);
configBinder(binder).bindConfig(NessieConfig.class);
+ configBinder(binder).bindConfig(IcebergRestConfig.class);
binder.bind(IcebergSessionProperties.class).in(Scopes.SINGLETON);
binder.bind(IcebergTableProperties.class).in(Scopes.SINGLETON);
diff --git a/presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergResourceFactory.java b/presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergResourceFactory.java
index 8b1c8911d8ef..fb84477265df 100644
--- a/presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergResourceFactory.java
+++ b/presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergResourceFactory.java
@@ -13,22 +13,27 @@
*/
package com.facebook.presto.iceberg;
+import com.facebook.presto.hive.NodeVersion;
import com.facebook.presto.hive.gcs.GcsConfigurationInitializer;
import com.facebook.presto.hive.s3.S3ConfigurationUpdater;
import com.facebook.presto.iceberg.nessie.NessieConfig;
+import com.facebook.presto.iceberg.rest.IcebergRestConfig;
import com.facebook.presto.spi.ConnectorSession;
import com.facebook.presto.spi.PrestoException;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.util.concurrent.UncheckedExecutionException;
+import io.jsonwebtoken.Jwts;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.iceberg.CatalogUtil;
import org.apache.iceberg.catalog.Catalog;
import org.apache.iceberg.catalog.SupportsNamespaces;
+import org.apache.iceberg.rest.auth.OAuth2Properties;
import javax.inject.Inject;
+import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -40,12 +45,18 @@
import static com.facebook.presto.iceberg.IcebergUtil.loadCachingProperties;
import static com.facebook.presto.iceberg.nessie.AuthenticationType.BASIC;
import static com.facebook.presto.iceberg.nessie.AuthenticationType.BEARER;
+import static com.facebook.presto.iceberg.rest.AuthenticationType.OAUTH2;
+import static com.facebook.presto.iceberg.rest.SessionType.USER;
import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED;
import static com.google.common.base.Throwables.throwIfInstanceOf;
import static com.google.common.base.Throwables.throwIfUnchecked;
+import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static org.apache.iceberg.CatalogProperties.FILE_IO_IMPL;
+import static org.apache.iceberg.CatalogProperties.URI;
import static org.apache.iceberg.CatalogProperties.WAREHOUSE_LOCATION;
+import static org.apache.iceberg.rest.auth.OAuth2Properties.CREDENTIAL;
+import static org.apache.iceberg.rest.auth.OAuth2Properties.TOKEN;
/**
* Factory for loading Iceberg resources such as Catalog.
@@ -63,9 +74,18 @@ public class IcebergResourceFactory
private final GcsConfigurationInitializer gcsConfigurationInitialize;
private final IcebergConfig icebergConfig;
+ private final IcebergRestConfig restConfig;
+ private final NodeVersion nodeVersion;
@Inject
- public IcebergResourceFactory(IcebergConfig config, IcebergCatalogName catalogName, NessieConfig nessieConfig, S3ConfigurationUpdater s3ConfigurationUpdater, GcsConfigurationInitializer gcsConfigurationInitialize)
+ public IcebergResourceFactory(
+ IcebergConfig config,
+ IcebergCatalogName catalogName,
+ NessieConfig nessieConfig,
+ IcebergRestConfig restConfig,
+ S3ConfigurationUpdater s3ConfigurationUpdater,
+ GcsConfigurationInitializer gcsConfigurationInitialize,
+ NodeVersion nodeVersion)
{
this.catalogName = requireNonNull(catalogName, "catalogName is null").getCatalogName();
this.icebergConfig = requireNonNull(config, "config is null");
@@ -73,8 +93,10 @@ public IcebergResourceFactory(IcebergConfig config, IcebergCatalogName catalogNa
this.catalogWarehouse = config.getCatalogWarehouse();
this.hadoopConfigResources = config.getHadoopConfigResources();
this.nessieConfig = requireNonNull(nessieConfig, "nessieConfig is null");
+ this.restConfig = requireNonNull(restConfig, "restConfig is null");
this.s3ConfigurationUpdater = requireNonNull(s3ConfigurationUpdater, "s3ConfigurationUpdater is null");
this.gcsConfigurationInitialize = requireNonNull(gcsConfigurationInitialize, "gcsConfigurationInitialize is null");
+ this.nodeVersion = requireNonNull(nodeVersion, "nodeVersion is null");
catalogCache = CacheBuilder.newBuilder()
.maximumSize(config.getCatalogCacheSize())
.build();
@@ -150,31 +172,63 @@ public Map getCatalogProperties(ConnectorSession session)
if (catalogWarehouse != null) {
properties.put(WAREHOUSE_LOCATION, catalogWarehouse);
}
- if (catalogType == NESSIE) {
- properties.put("ref", getNessieReferenceName(session));
- properties.put("uri", nessieConfig.getServerUri().orElseThrow(() -> new IllegalStateException("iceberg.nessie.uri must be set for Nessie")));
- String hash = getNessieReferenceHash(session);
- if (hash != null) {
- properties.put("ref.hash", hash);
- }
- nessieConfig.getReadTimeoutMillis().ifPresent(val -> properties.put("transport.read-timeout", val.toString()));
- nessieConfig.getConnectTimeoutMillis().ifPresent(val -> properties.put("transport.connect-timeout", val.toString()));
- nessieConfig.getClientBuilderImpl().ifPresent(val -> properties.put("client-builder-impl", val));
- nessieConfig.getAuthenticationType().ifPresent(type -> {
- if (type == BASIC) {
- properties.put("authentication.username", nessieConfig.getUsername()
- .orElseThrow(() -> new IllegalStateException("iceberg.nessie.auth.basic.username must be set with BASIC authentication")));
- properties.put("authentication.password", nessieConfig.getPassword()
- .orElseThrow(() -> new IllegalStateException("iceberg.nessie.auth.basic.password must be set with BASIC authentication")));
+ switch (catalogType) {
+ case NESSIE:
+ properties.put("ref", getNessieReferenceName(session));
+ properties.put("uri", nessieConfig.getServerUri().orElseThrow(() -> new IllegalStateException("iceberg.nessie.uri must be set for Nessie")));
+ String hash = getNessieReferenceHash(session);
+ if (hash != null) {
+ properties.put("ref.hash", hash);
}
- else if (type == BEARER) {
- properties.put("authentication.token", nessieConfig.getBearerToken()
- .orElseThrow(() -> new IllegalStateException("iceberg.nessie.auth.bearer.token must be set with BEARER authentication")));
+ nessieConfig.getReadTimeoutMillis().ifPresent(val -> properties.put("transport.read-timeout", val.toString()));
+ nessieConfig.getConnectTimeoutMillis().ifPresent(val -> properties.put("transport.connect-timeout", val.toString()));
+ nessieConfig.getClientBuilderImpl().ifPresent(val -> properties.put("client-builder-impl", val));
+ nessieConfig.getAuthenticationType().ifPresent(type -> {
+ if (type == BASIC) {
+ properties.put("authentication.username", nessieConfig.getUsername()
+ .orElseThrow(() -> new IllegalStateException("iceberg.nessie.auth.basic.username must be set with BASIC authentication")));
+ properties.put("authentication.password", nessieConfig.getPassword()
+ .orElseThrow(() -> new IllegalStateException("iceberg.nessie.auth.basic.password must be set with BASIC authentication")));
+ }
+ else if (type == BEARER) {
+ properties.put("authentication.token", nessieConfig.getBearerToken()
+ .orElseThrow(() -> new IllegalStateException("iceberg.nessie.auth.bearer.token must be set with BEARER authentication")));
+ }
+ });
+ if (!nessieConfig.isCompressionEnabled()) {
+ properties.put("transport.disable-compression", "true");
}
- });
- if (!nessieConfig.isCompressionEnabled()) {
- properties.put("transport.disable-compression", "true");
- }
+ break;
+ case REST:
+ properties.put(URI, restConfig.getServerUri().orElseThrow(
+ () -> new IllegalStateException("iceberg.rest.uri must be set for REST catalog")));
+ restConfig.getAuthenticationType().ifPresent(type -> {
+ if (type == OAUTH2) {
+ if (!restConfig.credentialOrTokenExists()) {
+ throw new IllegalStateException("iceberg.rest.auth.oauth2 requires either a credential or a token");
+ }
+ restConfig.getCredential().ifPresent(credential -> properties.put(CREDENTIAL, credential));
+ restConfig.getToken().ifPresent(token -> properties.put(TOKEN, token));
+ }
+ });
+ restConfig.getSessionType().ifPresent(type -> {
+ if (type == USER) {
+ properties.putAll(session.getIdentity().getExtraCredentials());
+
+ String sessionId = format("%s-%s", session.getUser(), session.getSource().orElse("default"));
+ String jwt = Jwts.builder()
+ .setId(sessionId)
+ .setSubject(session.getUser())
+ .setIssuedAt(new Date())
+ .setIssuer(nodeVersion.toString())
+ .claim("user", session.getUser())
+ .claim("source", session.getSource().orElse(""))
+ .compact();
+
+ properties.put(OAuth2Properties.JWT_TOKEN_TYPE, jwt);
+ }
+ });
+ break;
}
return properties;
}
diff --git a/presto-iceberg/src/main/java/com/facebook/presto/iceberg/rest/AuthenticationType.java b/presto-iceberg/src/main/java/com/facebook/presto/iceberg/rest/AuthenticationType.java
new file mode 100644
index 000000000000..95bd3d4f9955
--- /dev/null
+++ b/presto-iceberg/src/main/java/com/facebook/presto/iceberg/rest/AuthenticationType.java
@@ -0,0 +1,20 @@
+/*
+ * Licensed 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 com.facebook.presto.iceberg.rest;
+
+public enum AuthenticationType
+{
+ NONE,
+ OAUTH2
+}
diff --git a/presto-iceberg/src/main/java/com/facebook/presto/iceberg/rest/IcebergRestConfig.java b/presto-iceberg/src/main/java/com/facebook/presto/iceberg/rest/IcebergRestConfig.java
new file mode 100644
index 000000000000..b00c68f09aca
--- /dev/null
+++ b/presto-iceberg/src/main/java/com/facebook/presto/iceberg/rest/IcebergRestConfig.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed 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 com.facebook.presto.iceberg.rest;
+
+import com.facebook.airlift.configuration.Config;
+import com.facebook.airlift.configuration.ConfigDescription;
+
+import javax.validation.constraints.NotNull;
+
+import java.util.Optional;
+
+public class IcebergRestConfig
+{
+ private String serverUri;
+ private SessionType sessionType;
+ private AuthenticationType authenticationType;
+ private String credential;
+ private String token;
+
+ @NotNull
+ public Optional getServerUri()
+ {
+ return Optional.ofNullable(serverUri);
+ }
+
+ @Config("iceberg.rest.uri")
+ @ConfigDescription("The URI to connect to the REST server")
+ public IcebergRestConfig setServerUri(String serverUri)
+ {
+ this.serverUri = serverUri;
+ return this;
+ }
+
+ public Optional getSessionType()
+ {
+ return Optional.ofNullable(sessionType);
+ }
+
+ @Config("iceberg.rest.session.type")
+ @ConfigDescription("The session type to use for communicating with REST catalog server (NONE | USER)")
+ public IcebergRestConfig setSessionType(SessionType sessionType)
+ {
+ this.sessionType = sessionType;
+ return this;
+ }
+
+ public Optional getAuthenticationType()
+ {
+ return Optional.ofNullable(authenticationType);
+ }
+
+ @Config("iceberg.rest.auth.type")
+ @ConfigDescription("The authentication type to use for communicating with REST catalog server (NONE | OAUTH2)")
+ public IcebergRestConfig setAuthenticationType(AuthenticationType authenticationType)
+ {
+ this.authenticationType = authenticationType;
+ return this;
+ }
+
+ public Optional getCredential()
+ {
+ return Optional.ofNullable(credential);
+ }
+
+ @Config("iceberg.rest.auth.oauth2.credential")
+ @ConfigDescription("The credential to use for OAUTH2 authentication")
+ public IcebergRestConfig setCredential(String credential)
+ {
+ this.credential = credential;
+ return this;
+ }
+
+ public Optional getToken()
+ {
+ return Optional.ofNullable(token);
+ }
+
+ @Config("iceberg.rest.auth.oauth2.token")
+ @ConfigDescription("The Bearer token to use for OAUTH2 authentication")
+ public IcebergRestConfig setToken(String token)
+ {
+ this.token = token;
+ return this;
+ }
+
+ public boolean credentialOrTokenExists()
+ {
+ return credential != null || token != null;
+ }
+}
diff --git a/presto-iceberg/src/main/java/com/facebook/presto/iceberg/rest/SessionType.java b/presto-iceberg/src/main/java/com/facebook/presto/iceberg/rest/SessionType.java
new file mode 100644
index 000000000000..87e1fbee61cd
--- /dev/null
+++ b/presto-iceberg/src/main/java/com/facebook/presto/iceberg/rest/SessionType.java
@@ -0,0 +1,20 @@
+/*
+ * Licensed 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 com.facebook.presto.iceberg.rest;
+
+public enum SessionType
+{
+ NONE,
+ USER
+}
diff --git a/presto-iceberg/src/test/java/com/facebook/presto/iceberg/IcebergDistributedSmokeTestBase.java b/presto-iceberg/src/test/java/com/facebook/presto/iceberg/IcebergDistributedSmokeTestBase.java
index 3dac9dc34e35..8a8972fd8b2d 100644
--- a/presto-iceberg/src/test/java/com/facebook/presto/iceberg/IcebergDistributedSmokeTestBase.java
+++ b/presto-iceberg/src/test/java/com/facebook/presto/iceberg/IcebergDistributedSmokeTestBase.java
@@ -765,7 +765,7 @@ public void testCreateTableWithFormatVersion()
testWithAllFormatVersions(this::testCreateTableWithFormatVersion);
}
- private void testCreateTableWithFormatVersion(String formatVersion, String defaultDeleteMode)
+ protected void testCreateTableWithFormatVersion(String formatVersion, String defaultDeleteMode)
{
@Language("SQL") String createTable = "" +
"CREATE TABLE test_create_table_with_format_version_" + formatVersion + " " +
diff --git a/presto-iceberg/src/test/java/com/facebook/presto/iceberg/IcebergDistributedTestBase.java b/presto-iceberg/src/test/java/com/facebook/presto/iceberg/IcebergDistributedTestBase.java
index 824a519ef686..f96c395a3534 100644
--- a/presto-iceberg/src/test/java/com/facebook/presto/iceberg/IcebergDistributedTestBase.java
+++ b/presto-iceberg/src/test/java/com/facebook/presto/iceberg/IcebergDistributedTestBase.java
@@ -1385,7 +1385,7 @@ private void writeEqualityDeleteToNationTable(Table icebergTable, Map getConnectorProperties(CatalogType icebergCat
Path testDataDirectory = icebergDataDirectory.resolve(TEST_DATA_DIRECTORY);
switch (icebergCatalogType) {
case HADOOP:
+ case REST:
case NESSIE:
return ImmutableMap.of("iceberg.catalog.warehouse", testDataDirectory.getParent().toFile().toURI().toString());
case HIVE:
diff --git a/presto-iceberg/src/test/java/com/facebook/presto/iceberg/hadoop/TestIcebergSmokeHadoop.java b/presto-iceberg/src/test/java/com/facebook/presto/iceberg/hadoop/TestIcebergSmokeHadoop.java
index ed10e1468189..f55e5d46d92c 100644
--- a/presto-iceberg/src/test/java/com/facebook/presto/iceberg/hadoop/TestIcebergSmokeHadoop.java
+++ b/presto-iceberg/src/test/java/com/facebook/presto/iceberg/hadoop/TestIcebergSmokeHadoop.java
@@ -13,6 +13,7 @@
*/
package com.facebook.presto.iceberg.hadoop;
+import com.facebook.presto.hive.NodeVersion;
import com.facebook.presto.hive.gcs.HiveGcsConfig;
import com.facebook.presto.hive.gcs.HiveGcsConfigurationInitializer;
import com.facebook.presto.hive.s3.HiveS3Config;
@@ -23,6 +24,7 @@
import com.facebook.presto.iceberg.IcebergResourceFactory;
import com.facebook.presto.iceberg.IcebergUtil;
import com.facebook.presto.iceberg.nessie.NessieConfig;
+import com.facebook.presto.iceberg.rest.IcebergRestConfig;
import com.facebook.presto.spi.ConnectorSession;
import com.facebook.presto.spi.SchemaTableName;
import com.facebook.presto.tests.DistributedQueryRunner;
@@ -68,8 +70,10 @@ protected Table getIcebergTable(ConnectorSession session, String schema, String
IcebergResourceFactory resourceFactory = new IcebergResourceFactory(icebergConfig,
new IcebergCatalogName(ICEBERG_CATALOG),
new NessieConfig(),
+ new IcebergRestConfig(),
new PrestoS3ConfigurationUpdater(new HiveS3Config()),
- new HiveGcsConfigurationInitializer(new HiveGcsConfig()));
+ new HiveGcsConfigurationInitializer(new HiveGcsConfig()),
+ new NodeVersion("test_version"));
return IcebergUtil.getNativeIcebergTable(resourceFactory,
session,
diff --git a/presto-iceberg/src/test/java/com/facebook/presto/iceberg/nessie/TestIcebergSmokeNessie.java b/presto-iceberg/src/test/java/com/facebook/presto/iceberg/nessie/TestIcebergSmokeNessie.java
index 91e3c2214d43..3dbd44f018dc 100644
--- a/presto-iceberg/src/test/java/com/facebook/presto/iceberg/nessie/TestIcebergSmokeNessie.java
+++ b/presto-iceberg/src/test/java/com/facebook/presto/iceberg/nessie/TestIcebergSmokeNessie.java
@@ -13,6 +13,7 @@
*/
package com.facebook.presto.iceberg.nessie;
+import com.facebook.presto.hive.NodeVersion;
import com.facebook.presto.hive.gcs.HiveGcsConfig;
import com.facebook.presto.hive.gcs.HiveGcsConfigurationInitializer;
import com.facebook.presto.hive.s3.HiveS3Config;
@@ -23,6 +24,7 @@
import com.facebook.presto.iceberg.IcebergQueryRunner;
import com.facebook.presto.iceberg.IcebergResourceFactory;
import com.facebook.presto.iceberg.IcebergUtil;
+import com.facebook.presto.iceberg.rest.IcebergRestConfig;
import com.facebook.presto.spi.ConnectorSession;
import com.facebook.presto.spi.SchemaTableName;
import com.facebook.presto.testing.QueryRunner;
@@ -107,8 +109,10 @@ protected Table getIcebergTable(ConnectorSession session, String schema, String
IcebergResourceFactory resourceFactory = new IcebergResourceFactory(icebergConfig,
new IcebergCatalogName(ICEBERG_CATALOG),
nessieConfig,
+ new IcebergRestConfig(),
new PrestoS3ConfigurationUpdater(new HiveS3Config()),
- new HiveGcsConfigurationInitializer(new HiveGcsConfig()));
+ new HiveGcsConfigurationInitializer(new HiveGcsConfig()),
+ new NodeVersion("test_version"));
return IcebergUtil.getNativeIcebergTable(resourceFactory,
session,
diff --git a/presto-iceberg/src/test/java/com/facebook/presto/iceberg/rest/IcebergRestTestUtil.java b/presto-iceberg/src/test/java/com/facebook/presto/iceberg/rest/IcebergRestTestUtil.java
new file mode 100644
index 000000000000..d9edd97ad81d
--- /dev/null
+++ b/presto-iceberg/src/test/java/com/facebook/presto/iceberg/rest/IcebergRestTestUtil.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed 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 com.facebook.presto.iceberg.rest;
+
+import com.facebook.airlift.bootstrap.Bootstrap;
+import com.facebook.airlift.http.server.TheServlet;
+import com.facebook.airlift.http.server.testing.TestingHttpServer;
+import com.facebook.airlift.http.server.testing.TestingHttpServerModule;
+import com.facebook.airlift.node.NodeInfo;
+import com.facebook.presto.hive.HdfsContext;
+import com.facebook.presto.hive.HdfsEnvironment;
+import com.facebook.presto.spi.ConnectorSession;
+import com.facebook.presto.testing.TestingConnectorSession;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Binder;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.TypeLiteral;
+import org.apache.hadoop.fs.Path;
+import org.apache.iceberg.catalog.Catalog;
+import org.apache.iceberg.jdbc.JdbcCatalog;
+import org.apache.iceberg.rest.IcebergRestCatalogServlet;
+import org.apache.iceberg.rest.RESTCatalogAdapter;
+import org.apache.iceberg.rest.RESTSessionCatalog;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ThreadLocalRandom;
+
+import static com.facebook.presto.iceberg.CatalogType.REST;
+import static com.facebook.presto.iceberg.IcebergDistributedTestBase.getHdfsEnvironment;
+import static java.util.Objects.requireNonNull;
+import static org.apache.iceberg.CatalogProperties.URI;
+import static org.apache.iceberg.CatalogProperties.WAREHOUSE_LOCATION;
+
+public class IcebergRestTestUtil
+{
+ public static final ConnectorSession SESSION = new TestingConnectorSession(ImmutableList.of());
+
+ private IcebergRestTestUtil()
+ {
+ }
+
+ public static Map restConnectorProperties(String serverUri)
+ {
+ return ImmutableMap.of("iceberg.catalog.type", REST.name(), "iceberg.rest.uri", serverUri);
+ }
+
+ public static TestingHttpServer getRestServer(String location)
+ {
+ JdbcCatalog backingCatalog = new JdbcCatalog();
+ HdfsEnvironment hdfsEnvironment = getHdfsEnvironment();
+ backingCatalog.setConf(hdfsEnvironment.getConfiguration(new HdfsContext(SESSION), new Path(location)));
+
+ Map properties = ImmutableMap.builder()
+ .put(URI, "jdbc:h2:mem:test_" + System.nanoTime() + "_" + ThreadLocalRandom.current().nextInt())
+ .put(WAREHOUSE_LOCATION, location)
+ .put("jdbc.username", "user")
+ .put("jdbc.password", "password")
+ .build();
+ backingCatalog.initialize("rest_jdbc_backend", properties);
+
+ DelegateRestSessionCatalog delegate = new DelegateRestSessionCatalog(new RESTCatalogAdapter(backingCatalog), backingCatalog);
+ return delegate.getServerInstance();
+ }
+
+ public static class DelegateRestSessionCatalog
+ extends RESTSessionCatalog
+ {
+ public RESTCatalogAdapter adapter;
+ private final Catalog delegate;
+
+ public DelegateRestSessionCatalog(RESTCatalogAdapter adapter, Catalog delegate)
+ {
+ super(properties -> adapter, null);
+ this.adapter = requireNonNull(adapter, "adapter is null");
+ this.delegate = requireNonNull(delegate, "delegate catalog is null");
+ }
+
+ @Override
+ public void close()
+ throws IOException
+ {
+ super.close();
+ adapter.close();
+
+ if (delegate instanceof Closeable) {
+ ((Closeable) delegate).close();
+ }
+ }
+
+ public TestingHttpServer getServerInstance()
+ {
+ Bootstrap app = new Bootstrap(
+ new TestingHttpServerModule(),
+ new RestHttpServerModule());
+
+ Injector injector = app
+ .doNotInitializeLogging()
+ .initialize();
+
+ return injector.getInstance(TestingHttpServer.class);
+ }
+
+ private class RestHttpServerModule
+ implements Module
+ {
+ @Override
+ public void configure(Binder binder)
+ {
+ binder.bind(new TypeLiteral