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 5a9ee9aba8 [#12682] improve(paimon): document optional Hadoop S3A 
support for Paimon catalog (#12683)
5a9ee9aba8 is described below

commit 5a9ee9aba8fb15ebf6a913de58bdb6ec3f1a2895
Author: Qi Yu <[email protected]>
AuthorDate: Thu Sep 10 22:33:20 2026 +0800

    [#12682] improve(paimon): document optional Hadoop S3A support for Paimon 
catalog (#12683)
    
    ### What changes were proposed in this pull request?
    
    Document Hadoop S3A as an optional filesystem for Paimon catalogs,
    including the required dependencies and configuration.
    
    Add `hadoop-aws` to the test runtime classpath only, and retain unit and
    LocalStack integration coverage for an `s3a://` Paimon filesystem
    catalog.
    
    ### Why are the changes needed?
    
    Paimon native S3 support uses `s3://` with `paimon-s3`. Hadoop S3A is an
    optional Hadoop-compatible filesystem and requires version-aligned
    `hadoop-aws` and `aws-java-sdk-bundle` JARs in the Paimon catalog
    classpath.
    
    Packaging these dependencies by default would increase the Paimon
    catalog package by about 298 MB. Users who need `s3a://` can install
    them explicitly instead.
    
    Fix: #12682
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. The Paimon catalog documentation now explains how to enable and
    configure optional Hadoop S3A support.
    
    No API or property-key changes are introduced, and S3A dependencies are
    not included in the production runtime classpath.
    
    ### How was this patch tested?
    
    - `./gradlew --no-daemon :catalogs:catalog-lakehouse-paimon:test --tests
    
org.apache.gravitino.catalog.lakehouse.paimon.TestPaimonCatalog.testS3AFileSystemIsAvailable
    --tests
    
org.apache.gravitino.catalog.lakehouse.paimon.integration.test.CatalogPaimonS3AIT.testAccessS3AFileSystem
    -PskipDockerTests=false --rerun-tasks`
    - Verified that `runtimeClasspath` does not contain `hadoop-aws`.
    - Verified that `testRuntimeClasspath` contains `hadoop-aws:3.3.6` and
    `aws-java-sdk-bundle:1.12.367`.
---
 catalogs/catalog-lakehouse-paimon/build.gradle.kts |  47 ++++++++
 .../lakehouse/paimon/TestPaimonCatalog.java        |  10 ++
 .../integration/test/CatalogPaimonS3AIT.java       | 125 +++++++++++++++++++++
 docs/lakehouse-paimon-catalog.md                   |  39 ++++++-
 4 files changed, 220 insertions(+), 1 deletion(-)

diff --git a/catalogs/catalog-lakehouse-paimon/build.gradle.kts 
b/catalogs/catalog-lakehouse-paimon/build.gradle.kts
index 0ba959ba90..7b34b2812f 100644
--- a/catalogs/catalog-lakehouse-paimon/build.gradle.kts
+++ b/catalogs/catalog-lakehouse-paimon/build.gradle.kts
@@ -28,6 +28,14 @@ val scalaVersion: String = 
project.properties["scalaVersion"] as? String ?: extr
 val sparkVersion: String = libs.versions.spark35.get()
 val sparkMajorVersion: String = sparkVersion.substringBeforeLast(".")
 val paimonVersion: String = libs.versions.paimon.get()
+val s3aTestRuntime by configurations.creating {
+  isCanBeConsumed = false
+  isCanBeResolved = true
+}
+
+configurations.testRuntimeOnly {
+  extendsFrom(s3aTestRuntime)
+}
 
 dependencies {
   compileOnly(project(":api"))
@@ -210,9 +218,43 @@ dependencies {
   testImplementation(libs.testcontainers.localstack)
   testImplementation(libs.testcontainers.mysql)
 
+  // Keep optional Hadoop S3A dependencies available only to tests.
+  s3aTestRuntime(libs.hadoop3.aws)
   testRuntimeOnly(libs.junit.jupiter.engine)
 }
 
+val s3aTestLibDirectory =
+  "$rootDir/distribution/package-all/catalogs/lakehouse-paimon/libs"
+val copyS3ATestDependencies =
+  tasks.register("copyS3ATestDependencies") {
+    inputs.files(s3aTestRuntime)
+    outputs.files(
+      provider {
+        s3aTestRuntime.files.filter {
+          it.name.startsWith("hadoop-aws-") || 
it.name.startsWith("aws-java-sdk-bundle-")
+        }.map { file("$s3aTestLibDirectory/${it.name}") }
+      }
+    )
+    // Copy tasks targeting the distribution depend on 
cleanDistributionPackage. Test setup
+    // must preserve the existing deployment, so copy the optional jars in a 
regular task.
+    doLast {
+      copy {
+        from(s3aTestRuntime) {
+          include("hadoop-aws-*.jar", "aws-java-sdk-bundle-*.jar")
+        }
+        into(s3aTestLibDirectory)
+      }
+    }
+  }
+val cleanS3ATestDependencies =
+  tasks.register<Delete>("cleanS3ATestDependencies") {
+    delete(
+      fileTree(s3aTestLibDirectory) {
+        include("hadoop-aws-*.jar", "aws-java-sdk-bundle-*.jar")
+      }
+    )
+  }
+
 tasks {
   register("runtimeJars", Copy::class) {
     from(configurations.runtimeClasspath)
@@ -266,6 +308,11 @@ tasks.test {
   } else {
     dependsOn(tasks.jar)
   }
+
+  if (project.properties["testMode"] == "deploy") {
+    dependsOn(copyS3ATestDependencies)
+    finalizedBy(cleanS3ATestDependencies)
+  }
 }
 
 tasks.getByName("generateMetadataFileForMavenJavaPublication") {
diff --git 
a/catalogs/catalog-lakehouse-paimon/src/test/java/org/apache/gravitino/catalog/lakehouse/paimon/TestPaimonCatalog.java
 
b/catalogs/catalog-lakehouse-paimon/src/test/java/org/apache/gravitino/catalog/lakehouse/paimon/TestPaimonCatalog.java
index 93510faf29..998d1d9386 100644
--- 
a/catalogs/catalog-lakehouse-paimon/src/test/java/org/apache/gravitino/catalog/lakehouse/paimon/TestPaimonCatalog.java
+++ 
b/catalogs/catalog-lakehouse-paimon/src/test/java/org/apache/gravitino/catalog/lakehouse/paimon/TestPaimonCatalog.java
@@ -48,6 +48,8 @@ import org.apache.gravitino.rel.ViewCatalog;
 import org.apache.gravitino.storage.AzureProperties;
 import org.apache.gravitino.storage.OSSProperties;
 import org.apache.gravitino.storage.S3Properties;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FileSystem;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -146,6 +148,14 @@ public class TestPaimonCatalog {
                 ImmutableMap.of()));
   }
 
+  @Test
+  void testS3AFileSystemIsAvailable() throws IOException {
+    Class<? extends FileSystem> fileSystemClass =
+        FileSystem.getFileSystemClass("s3a", new Configuration());
+
+    Assertions.assertEquals("org.apache.hadoop.fs.s3a.S3AFileSystem", 
fileSystemClass.getName());
+  }
+
   @Test
   void testAsViewCatalog() {
     PaimonCatalog paimonCatalog = newPaimonCatalog("catalog_view");
diff --git 
a/catalogs/catalog-lakehouse-paimon/src/test/java/org/apache/gravitino/catalog/lakehouse/paimon/integration/test/CatalogPaimonS3AIT.java
 
b/catalogs/catalog-lakehouse-paimon/src/test/java/org/apache/gravitino/catalog/lakehouse/paimon/integration/test/CatalogPaimonS3AIT.java
new file mode 100644
index 0000000000..969c4e7e63
--- /dev/null
+++ 
b/catalogs/catalog-lakehouse-paimon/src/test/java/org/apache/gravitino/catalog/lakehouse/paimon/integration/test/CatalogPaimonS3AIT.java
@@ -0,0 +1,125 @@
+/*
+ * 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.catalog.lakehouse.paimon.integration.test;
+
+import static org.apache.gravitino.connector.BaseCatalog.CATALOG_BYPASS_PREFIX;
+
+import com.google.common.collect.Maps;
+import java.util.Collections;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.Schema;
+import 
org.apache.gravitino.catalog.lakehouse.paimon.PaimonCatalogPropertiesMetadata;
+import org.apache.gravitino.client.GravitinoMetalake;
+import 
org.apache.gravitino.integration.test.container.GravitinoLocalStackContainer;
+import org.apache.gravitino.integration.test.util.BaseIT;
+import org.apache.gravitino.integration.test.util.GravitinoITUtils;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.Container;
+
+/** Integration test for accessing a Paimon filesystem catalog through Hadoop 
S3A. */
+@Tag("gravitino-docker-test")
+public class CatalogPaimonS3AIT extends BaseIT {
+
+  private static final String PROVIDER = "lakehouse-paimon";
+  private static final String S3A_PROPERTY_PREFIX = CATALOG_BYPASS_PREFIX + 
"hadoop.fs.s3a.";
+  private static final String ACCESS_KEY = "test";
+  private static final String SECRET_KEY = "test";
+
+  private final String bucketName = "paimon-s3a-" + 
UUID.randomUUID().toString().replace("-", "");
+  private final String metalakeName = 
GravitinoITUtils.genRandomName("paimon_s3a_metalake");
+  private final String catalogName = 
GravitinoITUtils.genRandomName("paimon_s3a_catalog");
+  private final String schemaName = 
GravitinoITUtils.genRandomName("paimon_s3a_schema");
+
+  private GravitinoMetalake metalake;
+  private GravitinoLocalStackContainer localStackContainer;
+
+  @BeforeAll
+  void setUp() {
+    containerSuite.startLocalStackContainer();
+    localStackContainer = containerSuite.getLocalStackContainer();
+
+    Awaitility.await()
+        .atMost(60, TimeUnit.SECONDS)
+        .pollInterval(1, TimeUnit.SECONDS)
+        .until(
+            () -> {
+              try {
+                Container.ExecResult result =
+                    localStackContainer.executeInContainer(
+                        "awslocal", "s3", "mb", "s3://" + bucketName);
+                return result.getExitCode() == 0;
+              } catch (Exception e) {
+                return false;
+              }
+            });
+
+    metalake = client.createMetalake(metalakeName, "Paimon S3A metalake", 
Collections.emptyMap());
+  }
+
+  @AfterAll
+  void tearDown() {
+    if (metalake != null && metalake.catalogExists(catalogName)) {
+      metalake.disableCatalog(catalogName);
+      metalake.dropCatalog(catalogName, true);
+    }
+    if (client != null && client.metalakeExists(metalakeName)) {
+      client.disableMetalake(metalakeName);
+      client.dropMetalake(metalakeName, true);
+    }
+  }
+
+  @Test
+  void testAccessS3AFileSystem() {
+    Map<String, String> catalogProperties = Maps.newHashMap();
+    
catalogProperties.put(PaimonCatalogPropertiesMetadata.GRAVITINO_CATALOG_BACKEND,
 "filesystem");
+    catalogProperties.put(
+        PaimonCatalogPropertiesMetadata.WAREHOUSE, "s3a://" + bucketName + 
"/warehouse");
+    catalogProperties.put(S3A_PROPERTY_PREFIX + "access.key", ACCESS_KEY);
+    catalogProperties.put(S3A_PROPERTY_PREFIX + "secret.key", SECRET_KEY);
+    catalogProperties.put(
+        S3A_PROPERTY_PREFIX + "endpoint",
+        String.format(
+            "http://%s:%d";,
+            localStackContainer.getContainer().getHost(),
+            
localStackContainer.getMappedPort(GravitinoLocalStackContainer.PORT)));
+    catalogProperties.put(S3A_PROPERTY_PREFIX + "path.style.access", "true");
+    catalogProperties.put(S3A_PROPERTY_PREFIX + "connection.ssl.enabled", 
"false");
+
+    Catalog catalog =
+        metalake.createCatalog(
+            catalogName,
+            Catalog.Type.RELATIONAL,
+            PROVIDER,
+            "Paimon S3A catalog",
+            catalogProperties);
+    Schema schema =
+        catalog.asSchemas().createSchema(schemaName, "Paimon S3A schema", 
Collections.emptyMap());
+
+    Assertions.assertEquals(schemaName, schema.name());
+    Assertions.assertEquals(schemaName, 
catalog.asSchemas().loadSchema(schemaName).name());
+  }
+}
diff --git a/docs/lakehouse-paimon-catalog.md b/docs/lakehouse-paimon-catalog.md
index dc861dd6b2..2ebd10fe3e 100644
--- a/docs/lakehouse-paimon-catalog.md
+++ b/docs/lakehouse-paimon-catalog.md
@@ -56,7 +56,7 @@ Builds with Apache Paimon `1.2`.
 | `dlf-token-loader`                                 | The token loader for 
Aliyun DLF.                                                                     
                                                                                
                       | (none)                                                 
                        | No                                                    
                                                                                
                 [...]
 
 :::note
-- If you want to use the `oss` or `s3` warehouse, you need to place related 
jars in the `catalogs/lakehouse-paimon/lib` directory, more information can be 
found in the [Paimon 
S3](https://paimon.apache.org/docs/1.2/maintenance/filesystems/#s3).
+- If you want to use the `oss` or `s3` warehouse, you need to place related 
jars in the `catalogs/lakehouse-paimon/libs` directory, more information can be 
found in the [Paimon 
S3](https://paimon.apache.org/docs/1.2/maintenance/filesystems/#s3).
 - If you use an S3 warehouse, also download 
[`gravitino-aws-<version>.jar`](https://mvnrepository.com/artifact/org.apache.gravitino/gravitino-aws)
 and place it in the `catalogs/lakehouse-paimon/libs` directory to enable 
credential vending. For OSS, use 
[`gravitino-aliyun-<version>.jar`](https://mvnrepository.com/artifact/org.apache.gravitino/gravitino-aliyun)
 instead.
 - If you want to use REST backend, Gravitino Paimon catalog supports Aliyun 
DLF (Data Lake Formation) as the REST catalog service. You need to configure 
the DLF-related properties eg:
 ```
@@ -80,6 +80,43 @@ connect to Aliyun DLF, more information can be found in the 
[Paimon REST Catalog
 
 Any properties not defined by Gravitino with `gravitino.bypass.` prefix will 
pass to Paimon catalog properties and HDFS configuration. For example, if 
specify `gravitino.bypass.table.type`, `table.type` will pass to Paimon catalog 
properties.
 
+#### Hadoop S3A (optional)
+
+Paimon's native S3 filesystem uses an `s3://` warehouse and the `paimon-s3` 
dependency. As an
+alternative, you can use Hadoop S3A with an `s3a://` warehouse. Gravitino does 
not package the
+optional S3A dependencies by default. To enable it, add the following JARs to
+`catalogs/lakehouse-paimon/libs` and restart Gravitino:
+
+- `hadoop-aws-<hadoop-version>.jar`, with the same version as the Hadoop 
libraries packaged by
+  Gravitino.
+- The `aws-java-sdk-bundle` version required by that `hadoop-aws` release.
+
+The following example configures static credentials and a custom HTTP S3 
endpoint:
+
+```json
+{
+  "name": "paimon_s3a",
+  "type": "RELATIONAL",
+  "provider": "lakehouse-paimon",
+  "properties": {
+    "catalog-backend": "filesystem",
+    "warehouse": "s3a://bucket/warehouse",
+    "gravitino.bypass.hadoop.fs.s3a.access.key": "<access-key-id>",
+    "gravitino.bypass.hadoop.fs.s3a.secret.key": "<secret-access-key>",
+    "gravitino.bypass.hadoop.fs.s3a.endpoint": "http://s3.example.com";,
+    "gravitino.bypass.hadoop.fs.s3a.path.style.access": "true",
+    "gravitino.bypass.hadoop.fs.s3a.connection.ssl.enabled": "false"
+  }
+}
+```
+
+The endpoint-related properties are only needed for a custom endpoint. Omit
+`gravitino.bypass.hadoop.fs.s3a.connection.ssl.enabled` when the endpoint uses 
HTTPS, and enable
+path-style access only when the S3-compatible service requires it. Hadoop S3A 
automatically
+discovers `S3AFileSystem` and uses its default credential provider chain, so 
`fs.s3a.impl` and
+`fs.s3a.aws.credentials.provider` do not need to be configured for this 
example. You can pass other
+Hadoop S3A properties by prefixing them with `gravitino.bypass.hadoop.`.
+
 #### JDBC Backend
 
 If you are using JDBC backend, you must specify the properties like 
`jdbc-user`, `jdbc-password` and `jdbc-driver`.

Reply via email to