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

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


The following commit(s) were added to refs/heads/main by this push:
     new 3ec114aa28 [#13568] feat(iceberg): Add experimental Iceberg REST 
auxiliary service (#13569)
3ec114aa28 is described below

commit 3ec114aa28aee7cfc1672b5f1ba24993df25b511
Author: roryqi <[email protected]>
AuthorDate: Fri Oct 16 21:23:28 2026 +0800

    [#13568] feat(iceberg): Add experimental Iceberg REST auxiliary service 
(#13569)
    
    ### What changes were proposed in this pull request?
    
    - Add the `iceberg-rest-experimental-server` module.
    - Package the published `com.datastrato:experimental-iceberg-*` 1.13.0
    artifacts under a separate classpath.
    - Keep the packaged experimental server's shared Caffeine and Jackson
    dependencies aligned with Gravitino's pinned versions.
    - Add the experimental module to the Gravitino binary distribution and
    update its legal notices.
    - Add configuration documentation and log the effective Iceberg version
    and code source at startup.
    - Run the experimental authorization suite in a dedicated test task with
    the experimental Iceberg runtime and Spark 4.1/Scala 2.13. Keep the
    embedded Gravitino server on its javax Jersey/HK2 stack and disable the
    incompatible Spark UI servlet during this suite.
    
    ### Why are the changes needed?
    
    Developers need to test Iceberg read-restriction work against the
    Datastrato Iceberg distribution without replacing the regular Iceberg
    dependency set.
    
    The regular and experimental variants reuse the `iceberg-rest` auxiliary
    service name and configuration keys. Users select one variant through
    `gravitino.iceberg-rest.classpath`; the two variants cannot run side by
    side.
    
    Fix: #13568
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. The binary distribution adds the `iceberg-rest-experimental-server`
    directory. To use it, configure:
    
    ```properties
    gravitino.auxService.names = iceberg-rest
    gravitino.iceberg-rest.classpath = iceberg-rest-experimental-server/libs, 
iceberg-rest-experimental-server/conf
    ```
    
    ### How was this patch tested?
    
    - `./gradlew :iceberg:iceberg-rest-server:experimentalTest -PskipTests
    -PskipDockerTests=false -PtestMode=embedded`
    - `./gradlew :iceberg:iceberg-rest-server:test -PskipITs`
    - `./gradlew :iceberg:iceberg-rest-server:test -PskipTests
    -PskipDockerTests=false -PtestMode=embedded --tests
    
'org.apache.gravitino.iceberg.integration.test.IcebergTableAuthorizationIT.testLoadTable'`
    - `./gradlew :iceberg:iceberg-rest-server:compileTestJava
    :iceberg:iceberg-rest-experimental-server:copyDepends`
    - `./gradlew :iceberg:iceberg-rest-server:spotlessCheck
    :iceberg:iceberg-rest-experimental-server:spotlessCheck`
    - Verified that the experimental test ran 16 tests with no failures on
    Spark 4.1.3 and loaded the pinned Datastrato Iceberg commit from
    `com.datastrato:experimental-iceberg-api`.
    - Verified that the packaged experimental dependencies contain only
    `experimental-iceberg-*` 1.13.0 artifacts, Caffeine 2.9.3, and Jackson
    2.15.2 for the affected shared libraries.
---
 .../workflows/backend-integration-test-action.yml  |   2 +-
 NOTICE.bin                                         |  10 +-
 build.gradle.kts                                   |   1 +
 .../client-python/gravitino/utils/http_client.py   |   2 +-
 .../tests/unittests/test_http_client_headers.py    |  21 ++++
 conf/gravitino.conf.template                       |   2 +
 docs/iceberg-rest-service.md                       |  24 ++++-
 gradle/libs.versions.toml                          |   3 +
 .../build.gradle.kts                               |  93 ++++++++++++++++++
 iceberg/iceberg-rest-server/build.gradle.kts       | 106 +++++++++++++++++++-
 .../org/apache/gravitino/iceberg/RESTService.java  |   7 ++
 .../integration/test/IcebergAuthorizationIT.java   |   9 +-
 .../test/IcebergExperimentalAuthorizationIT.java   | 107 +++++++++++++++++++++
 .../test/IcebergTableAuthorizationIT.java          |  13 ++-
 .../integration/test/util/TestDatabaseName.java    |   3 +
 settings.gradle.kts                                |   1 +
 16 files changed, 392 insertions(+), 12 deletions(-)

diff --git a/.github/workflows/backend-integration-test-action.yml 
b/.github/workflows/backend-integration-test-action.yml
index b5de5e641e..d5cf1bd57e 100644
--- a/.github/workflows/backend-integration-test-action.yml
+++ b/.github/workflows/backend-integration-test-action.yml
@@ -72,7 +72,7 @@ jobs:
             EXCLUDE_CONTRIB_TESTS="$EXCLUDE_CONTRIB_TESTS -x 
:catalogs-contrib:$module:test"
           done
 
-          ./gradlew test -PskipTests -PtestMode=${{ inputs.test-mode }} 
-PjdbcBackend=${{ inputs.backend }} -PskipDockerTests=false -PskipWeb=true \
+          ./gradlew test :iceberg:iceberg-rest-server:experimentalTest 
-PskipTests -PtestMode=${{ inputs.test-mode }} -PjdbcBackend=${{ inputs.backend 
}} -PskipDockerTests=false -PskipWeb=true \
             -x :web:web:test -x :web:integration-test:test -x :web-v2:web:test 
-x :web-v2:integration-test:test -x :clients:client-python:test \
             -x :flink-connector:flink-common:test \
             -x :flink-connector:flink-1.19:test -x 
:flink-connector:flink-runtime-1.19:test \
diff --git a/NOTICE.bin b/NOTICE.bin
index a400cf9f48..dedddb7fc6 100644
--- a/NOTICE.bin
+++ b/NOTICE.bin
@@ -31,7 +31,7 @@ AWS EventStream for Java
 Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 
 Apache Iceberg
-Copyright 2017-2022 The Apache Software Foundation
+Copyright 2017-2026 The Apache Software Foundation
 
 Apache Kafka
 Copyright 2011-2023 The Apache Software Foundation
@@ -603,6 +603,14 @@ This project includes code from Kite, developed at 
Cloudera, Inc. with
 the following copyright notice:
   Copyright 2013 Cloudera Inc.
 
+This project includes code from Presto, developed at Facebook, with the
+following copyright notice:
+  Copyright 2016 Facebook and contributors.
+
+This project includes code from Delta Lake, developed at The Delta Lake 
Project,
+with the following copyright notice:
+  Copyright (2021) The Delta Lake Project Authors.
+
 Apache Ivy (TM)
 Copyright 2007-2014 The Apache Software Foundation
 
diff --git a/build.gradle.kts b/build.gradle.kts
index ea7cd68e2b..c2daee27e3 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1183,6 +1183,7 @@ tasks {
         "copyJobsLib",
         ":authorizations:copyLibAndConfig",
         ":iceberg:iceberg-rest-server:copyLibAndConfigs",
+        ":iceberg:iceberg-rest-server-experiment:copyLibAndConfigs",
         ":lance:lance-rest-server:copyLibAndConfigs",
         ":maintenance:optimizer:copyLibAndConfigs",
         ":plugins:idp-basic:copyLibAndConfigs",
diff --git a/clients/client-python/gravitino/utils/http_client.py 
b/clients/client-python/gravitino/utils/http_client.py
index a0caada82d..b9cac49891 100644
--- a/clients/client-python/gravitino/utils/http_client.py
+++ b/clients/client-python/gravitino/utils/http_client.py
@@ -172,7 +172,7 @@ class HTTPClient:
 
         if isinstance(data, (str, bytes)):
             request_data = data.encode("utf-8") if isinstance(data, str) else 
data
-            self._update_headers(self.JSON_HEADER)
+            request_headers = self._update_headers(self.JSON_HEADER)
         elif data:
             request_data = urlencode(data.to_dict()).encode()
             request_headers = self._update_headers(self.FORMDATA_HEADER)
diff --git a/clients/client-python/tests/unittests/test_http_client_headers.py 
b/clients/client-python/tests/unittests/test_http_client_headers.py
index e5ace7862a..65882cc5f2 100644
--- a/clients/client-python/tests/unittests/test_http_client_headers.py
+++ b/clients/client-python/tests/unittests/test_http_client_headers.py
@@ -123,6 +123,27 @@ class TestHTTPClientHeaders(unittest.TestCase):
         self.assertEqual("configured/type", configured_headers["Content-Type"])
         self.assertEqual(configured_headers, client.request_headers)
 
+    def test_raw_data_uses_request_local_headers(self):
+        client = HTTPClient(
+            "http://localhost:8090";, request_headers={"X-Configured": 
"configured"}
+        )
+
+        with patch("gravitino.utils.http_client.build_opener") as build_opener:
+            requests = self._capture_requests(
+                build_opener, [self._success_response(), 
self._success_response()]
+            )
+            client.post("/string", data="raw", headers={"X-Per-Request": 
"string"})
+            client.post("/bytes", data=b"raw")
+
+        self.assertEqual("raw", requests[0].data.decode("utf-8"))
+        self.assertEqual("string", self._header_value(requests[0], 
"X-Per-Request"))
+        self.assertIsNone(self._header_value(requests[1], "X-Per-Request"))
+        for request in requests:
+            self.assertEqual("configured", self._header_value(request, 
"X-Configured"))
+            self.assertEqual(
+                "application/json", self._header_value(request, "Content-Type")
+            )
+
     def test_failed_request_headers_do_not_leak(self):
         client = HTTPClient("http://localhost:8090";)
         error_body = (
diff --git a/conf/gravitino.conf.template b/conf/gravitino.conf.template
index af8a6c0651..ef26e8a0b5 100644
--- a/conf/gravitino.conf.template
+++ b/conf/gravitino.conf.template
@@ -111,6 +111,8 @@ gravitino.authorization.serviceAdmins = anonymous
 gravitino.auxService.names = iceberg-rest,lance-rest
 # Iceberg REST service classpath
 gravitino.iceberg-rest.classpath = iceberg-rest-server/libs, 
iceberg-rest-server/conf
+# To use the experimental Iceberg dependencies, replace the classpath above 
with:
+# gravitino.iceberg-rest.classpath = iceberg-rest-server-experiment/libs, 
iceberg-rest-server-experiment/conf
 # Iceberg REST service host
 gravitino.iceberg-rest.host = 0.0.0.0
 # Iceberg REST service http port
diff --git a/docs/iceberg-rest-service.md b/docs/iceberg-rest-service.md
index 098c6a7feb..d259b71ff6 100644
--- a/docs/iceberg-rest-service.md
+++ b/docs/iceberg-rest-service.md
@@ -102,14 +102,30 @@ The server-level 
`gravitino.fetchFile.blockUnsafeRemoteUri` configuration contro
 
 #### Auxiliary Service
 
-| Configuration item                 | Description                             
                                                                                
                                                                                
                               | Default value | Required |
-|------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|----------|
-| `gravitino.auxService.names`       | The auxiliary service name of the 
Gravitino Iceberg REST catalog service. Use **`iceberg-rest`**.                 
                                                                                
                                     | (none)        | Yes      |
-| `gravitino.iceberg-rest.classpath` | The classpath of the Gravitino Iceberg 
REST catalog service; includes the directory containing jars and configuration. 
It supports both absolute and relative paths, for example, 
`iceberg-rest-server/libs, iceberg-rest-server/conf` | (none)        | Yes      
|
+| Configuration item                 | Description                             
                                                                                
                                                                                
                                                                                
                                                    | Default value | Required |
+|------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|----------|
+| `gravitino.auxService.names`       | The auxiliary service name of the 
Gravitino Iceberg REST catalog service. Use **`iceberg-rest`**.                 
                                                                                
                                                                                
                                                          | (none)        | Yes 
     |
+| `gravitino.iceberg-rest.classpath` | The classpath of the Gravitino Iceberg 
REST catalog service. Use `iceberg-rest-server/libs, iceberg-rest-server/conf` 
for the regular Iceberg dependencies or `iceberg-rest-server-experiment/libs, 
iceberg-rest-server-experiment/conf` for the experimental dependencies. It 
supports both absolute and relative paths. | (none)        | Yes      |
 
 These settings apply only to `gravitino.conf`.
 Do not add them to the standalone server configuration.
 
+The auxiliary service name and all Iceberg REST configuration keys remain the 
same when using the
+experimental dependencies. Only select the experimental classpath:
+
+```properties
+gravitino.auxService.names = iceberg-rest
+gravitino.iceberg-rest.classpath = iceberg-rest-server-experiment/libs, 
iceberg-rest-server-experiment/conf
+```
+
+The selected directory is prepended to the Iceberg REST auxiliary service 
classpath. Only one of
+the regular and experimental directories can be active at a time. The 
selection takes effect when
+Iceberg classes are absent from the Gravitino server's base classpath; the 
binary distribution is
+packaged this way. The service logs the loaded Iceberg version and code source 
during startup so
+the effective runtime can be verified. The experimental directory uses the 
Datastrato Iceberg
+distribution published under the `com.datastrato` Maven group. Its version is 
pinned by
+`datastrato-iceberg` in `gradle/libs.versions.toml`, independently of the 
regular Iceberg version.
+
 #### HTTP Server
 
 | Configuration item                               | Description               
                                                                                
                                                                                
    | Default value                                                             
   | Required |
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9b2f9a9168..589b91b10d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -71,6 +71,8 @@ commons-configuration1 = "1.6"
 commons-dbcp2 = "2.11.0"
 caffeine = "2.9.3"
 iceberg = '1.11.0'
+datastrato-iceberg = "1.13.0"
+experimental-iceberg-git-commit = "e6118db078e09d960600c3c302e0f220457cb388"
 # Last Iceberg release compatible with Spark 3.3.
 iceberg4spark33 = "1.8.1"
 iceberg4spark35 = "1.11.0"
@@ -82,6 +84,7 @@ spark33 = "3.3.4"
 spark34 = "3.4.3"
 spark35 = "3.5.3"
 spark40 = "4.0.3"
+spark41 = "4.1.3"
 kyuubi4spark = "1.12.0"
 kyuubi4authz = "1.10.2"
 trino = '435'
diff --git a/iceberg/iceberg-rest-server-experiment/build.gradle.kts 
b/iceberg/iceberg-rest-server-experiment/build.gradle.kts
new file mode 100644
index 0000000000..b4be172ce8
--- /dev/null
+++ b/iceberg/iceberg-rest-server-experiment/build.gradle.kts
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+description = "iceberg-rest-server-experiment"
+
+plugins {
+  `maven-publish`
+  id("java")
+  id("idea")
+}
+
+val datastratoIcebergVersion: String = libs.versions.datastrato.iceberg.get()
+val caffeineVersion: String = libs.versions.caffeine.get()
+val jacksonVersion: String = libs.versions.jackson.get()
+
+configurations.configureEach {
+  resolutionStrategy {
+    force(
+      "com.github.ben-manes.caffeine:caffeine:$caffeineVersion",
+      "com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion",
+      "com.fasterxml.jackson.core:jackson-core:$jacksonVersion",
+      "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion",
+      "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$jacksonVersion",
+      "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion"
+    )
+    eachDependency {
+      if (
+        requested.name.startsWith("iceberg-") &&
+        (requested.group == "org.apache.iceberg" || requested.group == 
"com.datastrato")
+      ) {
+        
useTarget("com.datastrato:experimental-${requested.name}:$datastratoIcebergVersion")
+        because("Use the Datastrato Iceberg distribution in the experimental 
auxiliary service")
+      }
+    }
+  }
+}
+
+dependencies {
+  implementation(project(":iceberg:iceberg-rest-server"))
+  implementation(libs.bundles.iceberg)
+}
+
+tasks {
+  val copyDepends by registering(Sync::class) {
+    dependsOn(":iceberg:iceberg-rest-server:copyDepends")
+    from(configurations.runtimeClasspath)
+    into(layout.buildDirectory.dir("dependencies"))
+  }
+
+  val copyLibs by registering(Sync::class) {
+    dependsOn(copyDepends, "build")
+    from(jar)
+    from(copyDepends)
+    into("$rootDir/distribution/package/iceberg-rest-server-experiment/libs")
+  }
+
+  val copyConfigs by registering(Copy::class) {
+    from(project(":iceberg:iceberg-rest-server").file("src/main/resources"))
+    into("$rootDir/distribution/package/iceberg-rest-server-experiment/conf")
+
+    include("core-site.xml.template")
+    include("hdfs-site.xml.template")
+
+    rename { original ->
+      if (original.endsWith(".template")) {
+        original.replace(".template", "")
+      } else {
+        original
+      }
+    }
+
+    fileMode = 0b111101101
+  }
+
+  register("copyLibAndConfigs") {
+    dependsOn(copyLibs, copyConfigs)
+  }
+}
diff --git a/iceberg/iceberg-rest-server/build.gradle.kts 
b/iceberg/iceberg-rest-server/build.gradle.kts
index 20a8abe8a9..9bc8637480 100644
--- a/iceberg/iceberg-rest-server/build.gradle.kts
+++ b/iceberg/iceberg-rest-server/build.gradle.kts
@@ -16,6 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import org.gradle.api.attributes.Attribute
+
 description = "iceberg-rest-service"
 
 plugins {
@@ -27,8 +29,75 @@ plugins {
 val scalaVersion: String = project.properties["scalaVersion"] as? String ?: 
extra["defaultScalaVersion"].toString()
 val sparkVersion: String = libs.versions.spark34.get()
 val sparkMajorVersion: String = sparkVersion.substringBeforeLast(".")
+val experimentalScalaVersion = "2.13"
+val experimentalSparkVersion: String = libs.versions.spark41.get()
+val experimentalSparkMajorVersion: String = 
experimentalSparkVersion.substringBeforeLast(".")
 val icebergVersion: String = libs.versions.iceberg.get()
+val datastratoIcebergVersion: String = libs.versions.datastrato.iceberg.get()
+val datastratoIcebergGitCommit: String = 
libs.versions.experimental.iceberg.git.commit.get()
 val scalaCollectionCompatVersion: String = 
libs.versions.scala.collection.compat.get()
+val caffeineVersion: String = libs.versions.caffeine.get()
+val jerseyVersion: String = libs.versions.jersey.get()
+val javaxWsRsVersion: String = libs.versions.javax.ws.rs.api.get()
+val hk2Version = "2.6.1"
+val jakartaValidationVersion = "2.0.2"
+
+val testRuntimeClasspath = configurations.testRuntimeClasspath.get()
+val experimentalTestRuntimeClasspath by configurations.creating {
+  isCanBeConsumed = false
+  isCanBeResolved = true
+  extendsFrom(testRuntimeClasspath)
+  attributes {
+    testRuntimeClasspath.attributes.keySet().forEach { key ->
+      @Suppress("UNCHECKED_CAST")
+      val typedKey = key as Attribute<Any>
+      attribute(typedKey, 
testRuntimeClasspath.attributes.getAttribute(typedKey)!!)
+    }
+  }
+  resolutionStrategy {
+    force(
+      "com.github.ben-manes.caffeine:caffeine:$caffeineVersion",
+      "javax.ws.rs:javax.ws.rs-api:$javaxWsRsVersion",
+      "org.glassfish.jersey.core:jersey-server:$jerseyVersion",
+      "org.glassfish.jersey.core:jersey-common:$jerseyVersion",
+      "org.glassfish.jersey.core:jersey-client:$jerseyVersion",
+      
"org.glassfish.jersey.containers:jersey-container-servlet:$jerseyVersion",
+      
"org.glassfish.jersey.containers:jersey-container-servlet-core:$jerseyVersion",
+      
"org.glassfish.jersey.containers:jersey-container-jetty-http:$jerseyVersion",
+      "org.glassfish.jersey.inject:jersey-hk2:$jerseyVersion",
+      "org.glassfish.hk2:hk2-api:$hk2Version",
+      "org.glassfish.hk2:hk2-locator:$hk2Version",
+      "org.glassfish.hk2:hk2-utils:$hk2Version",
+      "org.glassfish.hk2.external:aopalliance-repackaged:$hk2Version",
+      "org.glassfish.hk2.external:jakarta.inject:$hk2Version",
+      "jakarta.validation:jakarta.validation-api:$jakartaValidationVersion"
+    )
+    eachDependency {
+      if (requested.group == "org.apache.spark") {
+        val experimentalSparkModule = requested.name.replace("_$scalaVersion", 
"_$experimentalScalaVersion")
+        
useTarget("${requested.group}:$experimentalSparkModule:$experimentalSparkVersion")
+        because("The experimental Iceberg Spark runtime requires Spark 4.1 
with Scala 2.13")
+      } else if (
+        requested.group == "org.scala-lang.modules" && 
requested.name.endsWith("_$scalaVersion")
+      ) {
+        val experimentalScalaModule = requested.name.replace("_$scalaVersion", 
"_$experimentalScalaVersion")
+        
useTarget("${requested.group}:$experimentalScalaModule:${requested.version}")
+        because("Spark 4.1 uses Scala 2.13")
+      } else if (requested.name.startsWith("iceberg-spark-runtime-")) {
+        useTarget(
+          
"com.datastrato:experimental-iceberg-spark-runtime-${experimentalSparkMajorVersion}_$experimentalScalaVersion:$datastratoIcebergVersion"
+        )
+        because("Run experimental Iceberg integration tests with the matching 
Spark runtime")
+      } else if (
+        requested.name.startsWith("iceberg-") &&
+        (requested.group == "org.apache.iceberg" || requested.group == 
"com.datastrato")
+      ) {
+        
useTarget("com.datastrato:experimental-${requested.name}:$datastratoIcebergVersion")
+        because("Run experimental Iceberg integration tests with the 
Datastrato distribution")
+      }
+    }
+  }
+}
 
 dependencies {
   implementation(project(":api"))
@@ -98,6 +167,7 @@ dependencies {
   }
 
   testImplementation(libs.awaitility)
+  testImplementation(libs.javax.ws.rs.api)
   // At runtime use the SDK supplied by iceberg-aws-bundle, keeping its S3 and 
STS versions aligned.
   testCompileOnly(libs.aws.s3)
   testImplementation(libs.h2db)
@@ -199,13 +269,47 @@ tasks {
   }
 }
 
+val experimentalTest by tasks.registering(Test::class) {
+  description = "Runs Iceberg REST tests that require the experimental Iceberg 
runtime"
+  group = "verification"
+  testClassesDirs = sourceSets.test.get().output.classesDirs
+  classpath =
+    sourceSets.main.get().output +
+    sourceSets.test.get().output +
+    experimentalTestRuntimeClasspath
+  systemProperty("datastratoIcebergGitCommit", datastratoIcebergGitCommit)
+  systemProperty("spark.ui.enabled", "false")
+  setIncludes(
+    setOf(
+      "**/IcebergExperimentalAuthorizationIT.class"
+    )
+  )
+  filter {
+    includeTestsMatching(
+      
"org.apache.gravitino.iceberg.integration.test.IcebergExperimentalAuthorizationIT"
+    )
+  }
+  mustRunAfter(tasks.test)
+  dependsOn(
+    tasks.testClasses,
+    tasks.jar,
+    ":catalogs:catalog-lakehouse-iceberg:jar",
+    ":iceberg:iceberg-rest-server-experiment:copyDepends",
+    ":iceberg:iceberg-rest-server-experiment:jar"
+  )
+}
+
 tasks.test {
   val skipITs = project.hasProperty("skipITs")
+  exclude("**/IcebergExperimentalAuthorizationIT.class")
   if (skipITs) {
     // Exclude integration tests
     exclude("**/integration/test/**")
   } else {
-    dependsOn(tasks.jar)
+    dependsOn(
+      tasks.jar,
+      ":catalogs:catalog-lakehouse-iceberg:jar"
+    )
   }
 }
 
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
index 1786ba3e0e..99077475d9 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
@@ -19,6 +19,7 @@
 package org.apache.gravitino.iceberg;
 
 import com.google.common.collect.Lists;
+import java.security.CodeSource;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -65,6 +66,7 @@ import org.apache.gravitino.server.web.JettyServerConfig;
 import org.apache.gravitino.server.web.OutOfMemoryErrorListener;
 import org.apache.gravitino.server.web.RequestContextFilter;
 import 
org.apache.gravitino.server.web.filter.IcebergRESTAuthInterceptionService;
+import org.apache.iceberg.IcebergBuild;
 import org.glassfish.hk2.api.InterceptionService;
 import org.glassfish.hk2.utilities.binding.AbstractBinder;
 import org.glassfish.jersey.jackson.JacksonFeature;
@@ -268,6 +270,11 @@ public class RESTService implements 
GravitinoAuxiliaryService {
   @Override
   public void serviceInit(Map<String, String> properties, boolean auxMode) {
     this.auxMode = auxMode;
+    CodeSource icebergCodeSource = 
IcebergBuild.class.getProtectionDomain().getCodeSource();
+    LOG.info(
+        "Iceberg REST service runtime: {}, loaded from {}",
+        IcebergBuild.fullVersion(),
+        icebergCodeSource == null ? "unknown" : 
icebergCodeSource.getLocation());
     IcebergConfig icebergConfig = new IcebergConfig(properties);
     initServer(icebergConfig);
     LOG.info("Iceberg REST service init. Running in {} mode", auxMode ? 
"auxiliary" : "standalone");
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergAuthorizationIT.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergAuthorizationIT.java
index bd69a06659..aba2d127aa 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergAuthorizationIT.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergAuthorizationIT.java
@@ -95,7 +95,7 @@ public class IcebergAuthorizationIT extends BaseIT {
   @BeforeAll
   @Override
   public void startIntegrationTest() throws Exception {
-    
containerSuite.startPostgreSQLContainer(TestDatabaseName.PG_ICEBERG_AUTHZ_IT);
+    containerSuite.startPostgreSQLContainer(getTestDatabaseName());
     startGravitinoServerWithIcebergREST();
     initMetalakeAndCatalog();
     initSparkEnv();
@@ -219,6 +219,11 @@ public class IcebergAuthorizationIT extends BaseIT {
     return null;
   }
 
+  /** Returns the PostgreSQL database used by this authorization test suite. */
+  protected TestDatabaseName getTestDatabaseName() {
+    return TestDatabaseName.PG_ICEBERG_AUTHZ_IT;
+  }
+
   void revokeUserRoles() {
     List<String> roles = 
metalakeClientWithAllPrivilege.getUser(NORMAL_USER).roles();
     if (roles.size() > 0) {
@@ -341,7 +346,7 @@ public class IcebergAuthorizationIT extends BaseIT {
   }
 
   private String getPGUri() {
-    return 
containerSuite.getPostgreSQLContainer().getJdbcUrl(TestDatabaseName.PG_ICEBERG_AUTHZ_IT);
+    return 
containerSuite.getPostgreSQLContainer().getJdbcUrl(getTestDatabaseName());
   }
 
   private String getPGUser() {
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergExperimentalAuthorizationIT.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergExperimentalAuthorizationIT.java
new file mode 100644
index 0000000000..95de77ca7a
--- /dev/null
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergExperimentalAuthorizationIT.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.gravitino.iceberg.integration.test;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.apache.gravitino.integration.test.util.ITUtils;
+import org.apache.gravitino.integration.test.util.TestDatabaseName;
+import org.apache.iceberg.IcebergBuild;
+import org.apache.spark.sql.AnalysisException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.function.Executable;
+
+/**
+ * Integration test for Spark authorization through the Iceberg REST service 
loaded from the
+ * experimental classpath.
+ *
+ * <p>The inherited tests exercise table creation, loading, updates, deletion, 
listing, renaming,
+ * nested namespaces, and table privilege enforcement against the experimental 
Iceberg artifacts.
+ */
+@Tag("gravitino-docker-test")
+public class IcebergExperimentalAuthorizationIT extends 
IcebergTableAuthorizationIT {
+
+  private static final String EXPERIMENTAL_SERVER_DIRECTORY = 
"iceberg-rest-server-experiment";
+
+  @BeforeAll
+  @Override
+  public void startIntegrationTest() throws Exception {
+    Assertions.assertEquals(
+        System.getProperty("datastratoIcebergGitCommit"),
+        IcebergBuild.gitCommitId(),
+        "The experimental integration test must run with the Datastrato 
Iceberg runtime");
+
+    String rootDirectory = System.getenv("GRAVITINO_ROOT_DIR");
+    String experimentalServerHome =
+        Paths.get(rootDirectory, "iceberg", 
EXPERIMENTAL_SERVER_DIRECTORY).toString();
+    customConfigs.put(
+        "gravitino.iceberg-rest.classpath",
+        String.join(
+            ",",
+            Paths.get(experimentalServerHome, "build", "libs").toString(),
+            Paths.get(experimentalServerHome, "build", 
"dependencies").toString()));
+
+    Path serverLog = getServerLog();
+    long logOffset = Files.exists(serverLog) ? Files.size(serverLog) : 0;
+    super.startIntegrationTest();
+
+    String startupLog = readLogFrom(serverLog, logOffset);
+    Assertions.assertTrue(
+        startupLog.contains(System.getProperty("datastratoIcebergGitCommit")),
+        "The Iceberg REST service must load the Datastrato Iceberg runtime");
+    if 
(ITUtils.DEPLOY_TEST_MODE.equals(System.getProperty(ITUtils.TEST_MODE))) {
+      Assertions.assertTrue(
+          startupLog.contains(EXPERIMENTAL_SERVER_DIRECTORY),
+          "The deployed Iceberg REST service must load its configured 
experimental classpath");
+    }
+  }
+
+  @Override
+  protected TestDatabaseName getTestDatabaseName() {
+    return TestDatabaseName.PG_ICEBERG_EXPERIMENTAL_AUTHZ_IT;
+  }
+
+  /**
+   * Spark 4.1 wraps a missing table in a subclass of {@link 
AnalysisException}, unlike Spark 3.4.
+   *
+   * @param executable the table load operation
+   */
+  @Override
+  protected void assertNoSuchTableException(Executable executable) {
+    Assertions.assertThrows(AnalysisException.class, executable);
+  }
+
+  private Path getServerLog() {
+    if 
(ITUtils.DEPLOY_TEST_MODE.equals(System.getProperty(ITUtils.TEST_MODE))) {
+      return Paths.get(System.getenv("GRAVITINO_HOME"), "logs", 
"gravitino-server.log");
+    }
+    return Paths.get(System.getProperty("gravitino.log.path"));
+  }
+
+  private String readLogFrom(Path logFile, long offset) throws IOException {
+    byte[] logContent = Files.readAllBytes(logFile);
+    int start = logContent.length >= offset ? Math.toIntExact(offset) : 0;
+    return new String(logContent, start, logContent.length - start, 
StandardCharsets.UTF_8);
+  }
+}
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergTableAuthorizationIT.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergTableAuthorizationIT.java
index 0d19211fc7..bc2d4c12af 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergTableAuthorizationIT.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergTableAuthorizationIT.java
@@ -45,6 +45,7 @@ import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
 
 @Tag("gravitino-docker-test")
 public class IcebergTableAuthorizationIT extends IcebergAuthorizationIT {
@@ -57,6 +58,15 @@ public class IcebergTableAuthorizationIT extends 
IcebergAuthorizationIT {
     catalogClientWithAllPrivilege.asSchemas().createSchema(SCHEMA_NAME, 
"test", new HashMap<>());
   }
 
+  /**
+   * Verifies the exception thrown when an authorized user loads a table that 
does not exist.
+   *
+   * @param executable the table load operation
+   */
+  protected void assertNoSuchTableException(Executable executable) {
+    Assertions.assertThrowsExactly(AnalysisException.class, executable);
+  }
+
   @BeforeEach
   void revokePrivilege() {
     revokeUserRoles();
@@ -272,8 +282,7 @@ public class IcebergTableAuthorizationIT extends 
IcebergAuthorizationIT {
     setSchemaOwner(NORMAL_USER);
     Assertions.assertDoesNotThrow(() -> sql("DESC TABLE %s", tableName));
     // With ownership: non-existent table returns 404 (AnalysisException)
-    Assertions.assertThrowsExactly(
-        AnalysisException.class, () -> sql("DESC TABLE %s_not_exist", 
tableName));
+    assertNoSuchTableException(() -> sql("DESC TABLE %s_not_exist", 
tableName));
 
     setSchemaOwner(SUPER_USER);
     Assertions.assertThrowsExactly(ForbiddenException.class, () -> sql("DESC 
TABLE %s", tableName));
diff --git 
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/TestDatabaseName.java
 
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/TestDatabaseName.java
index e127c0bf6f..52f1c8c4f7 100644
--- 
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/TestDatabaseName.java
+++ 
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/TestDatabaseName.java
@@ -133,6 +133,9 @@ public enum TestDatabaseName {
   },
   PG_ICEBERG_AUTHZ_IT,
 
+  /** Represents the PostgreSQL database for experimental Iceberg 
authorization tests. */
+  PG_ICEBERG_EXPERIMENTAL_AUTHZ_IT,
+
   PG_ICEBERG_ASYNC_CLEANUP_IT,
 
   CLICKHOUSE_CLICKHOUSE_ABSTRACT_IT,
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 511cdd025d..4d15b84397 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -63,6 +63,7 @@ include(
 // in-tree for reference; see clients/filesystem-fuse/README.md for details.
 include("iceberg:iceberg-common")
 include("iceberg:iceberg-rest-server")
+include("iceberg:iceberg-rest-server-experiment")
 include("iceberg:iceberg-rest-trino-it")
 include("lance:lance-common")
 include("lance:lance-rest-server")

Reply via email to