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 69edf53a10  [#11748] feat(catalog-hadoop): support Tencent Cloud COS 
as fileset storage backend (Java side) (#11713)
69edf53a10 is described below

commit 69edf53a10759e3cb13767fee25b3b3cabd200cf
Author: Gary Wang <[email protected]>
AuthorDate: Fri Jul 3 15:33:05 2026 +0800

     [#11748] feat(catalog-hadoop): support Tencent Cloud COS as fileset 
storage backend (Java side) (#11713)
    
    ### What changes were proposed in this pull request?
    
    Add Tencent Cloud Object Storage (COS) support for the fileset catalog.
    The layout follows the existing OSS / S3 / GCS / Azure modules.
    
    - `bundles/tencent`: source module containing
    - `COSFileSystemProvider`: builds a Hadoop `CosNFileSystem` for the
    `cosn://` scheme.
    - `COSCredentialsProvider`: Hadoop-side provider that bridges
    Gravitino-vended `COSSecretKey` credentials to `hadoop-cos`.
    - `COSSecretKeyProvider`: server-side `CredentialProvider` that returns
    long-lived COS Secret Key credentials.
    - `COSUtils`: helper that maps Gravitino properties to Hadoop
    `Configuration`.
    - `bundles/tencent-bundle`: shaded fat-jar
    (`gravitino-tencent-bundle-<version>.jar`) that bundles `hadoop-cos` and
    `cos_api-bundle` with relocated packages.
    - `api`: new `COSSecretKeyCredential` type.
    - `catalogs/catalog-common`: `COSProperties` and `COSCredentialConfig`.
    - `clients/filesystem-hadoop3-runtime`: include `bundles/tencent` in the
    GVFS runtime jar.
    - `docs`: new `fileset-catalog-with-cos.md` and a cross-link from
    `fileset-catalog.md`.
    - Tests: `TestCOSFileSystemProvider`, `TestCOSCredentialProvider`, and
    `FilesetCOSCatalogIT`. The IT is skipped on CI when no real COS
    credentials are provided, the same way `FilesetOSSCatalogIT` is.
    
    ### Why are the changes needed?
    
    #6490 tracks adding Tencent COS support. Without it, users on Tencent
    Cloud have to either rely on S3-compatible workarounds, which do not
    work with Gravitino's credential vending, or maintain their own fork.
    
    This PR is the first of a chain of PRs against `main`:
    
    - PR-A (this PR): Java side, with the static `cos-secret-key`
    credential.
    - PR-B: server-side STS credential vending (`cos-token`).
    - PR-C: Python GVFS support (`COSStorageHandler` and Python-side
    `COSSecretKeyCredential`).
    - PR-D: Python `COSTokenCredential` wiring. Can be folded into PR-C if
    reviewers prefer fewer PRs.
    
    Part of #6490
    Fix: #11748
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, additive only. Existing fileset / catalog behavior is unchanged.
    New surface:
    
    - New fileset location scheme: `cosn://<bucket>/<path>`.
    - New fileset catalog properties: `cos-region`, `cos-endpoint`
    (optional, defaults to `cos.${region}.myqcloud.com`),
    `cos-access-key-id`, `cos-secret-access-key`.
    - New credential provider value: `credential-providers=cos-secret-key`
    (static AK/SK; STS will follow in PR-B).
    - New deployable artifact: `gravitino-tencent-bundle-<version>.jar`.
    - New documentation page: [Fileset Catalog with
    COS](docs/fileset-catalog-with-cos.md).
    
    ### How was this patch tested?
    
    - Unit tests: `:bundles:tencent:test`, `:catalogs:catalog-common:test`,
    `:catalogs:catalog-fileset:test`, `:api:test` all pass locally.
    - Build: `:bundles:tencent-bundle:shadowJar`,
    `:clients:filesystem-hadoop3-runtime:shadowJar`, and `assemble` for all
    touched modules pass locally.
    - Spotless and Checkstyle pass.
    - `FilesetCOSCatalogIT` passes locally against a real COS bucket in
    `ap-guangzhou`. It is skipped on CI when no credentials are provided.
    - Manual end-to-end verification on a local Gravitino server with the
    new bundle jars: created a fileset catalog backed by `cosn://`, then
    exercised `hadoop fs -ls`, `-put`, `-get`, `-cat`, `-rm` over
    `gvfs://fileset/...`, plus schema/fileset CRUD via the REST API and
    direct `cosn://...` access.
---
 .../credential/COSSecretKeyCredential.java         | 120 +++++
 .../org.apache.gravitino.credential.Credential     |   1 +
 bundles/tencent-bundle/build.gradle.kts            |  78 +++
 bundles/tencent/build.gradle.kts                   |  58 +++
 .../cos/credential/COSSecretKeyProvider.java       |  60 +++
 .../gravitino/cos/fs/COSCredentialsProvider.java   |  91 ++++
 .../gravitino/cos/fs/COSFileSystemProvider.java    | 128 +++++
 .../java/org/apache/gravitino/cos/fs/COSUtils.java |  45 ++
 ....gravitino.catalog.hadoop.fs.FileSystemProvider |  10 +-
 ....apache.gravitino.credential.CredentialProvider |  11 +-
 .../cos/credential/TestCOSCredentialProvider.java  |  63 +++
 .../cos/fs/TestCOSCredentialsProvider.java         | 154 ++++++
 .../cos/fs/TestCOSFileSystemProvider.java          |  92 ++++
 .../credential/config/COSCredentialConfig.java     |  79 +++
 .../apache/gravitino/storage/COSProperties.java    |  34 ++
 catalogs/catalog-fileset/build.gradle.kts          |   2 +
 .../integration/test/FilesetCOSCatalogIT.java      | 207 ++++++++
 .../gravitino/catalog/hadoop/fs/Constants.java     |   6 +
 .../filesystem-hadoop3-runtime/build.gradle.kts    |   1 +
 clients/filesystem-hadoop3/build.gradle.kts        |   2 +
 .../test/GravitinoVirtualFileSystemCOSIT.java      | 169 +++++++
 docs/fileset-catalog-with-cos.md                   | 544 +++++++++++++++++++++
 docs/fileset-catalog.md                            |  12 +-
 gradle/libs.versions.toml                          |   2 +
 settings.gradle.kts                                |   1 +
 25 files changed, 1946 insertions(+), 24 deletions(-)

diff --git 
a/api/src/main/java/org/apache/gravitino/credential/COSSecretKeyCredential.java 
b/api/src/main/java/org/apache/gravitino/credential/COSSecretKeyCredential.java
new file mode 100644
index 0000000000..a23c7d5c1e
--- /dev/null
+++ 
b/api/src/main/java/org/apache/gravitino/credential/COSSecretKeyCredential.java
@@ -0,0 +1,120 @@
+/*
+ *  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.credential;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+
+/** Tencent Cloud COS secret key credential. */
+public class COSSecretKeyCredential implements Credential {
+
+  /** COS secret key credential type. */
+  public static final String COS_SECRET_KEY_CREDENTIAL_TYPE = "cos-secret-key";
+  /** The static access key ID (a.k.a. SecretId in Tencent Cloud) used to 
access COS data. */
+  public static final String GRAVITINO_COS_STATIC_ACCESS_KEY_ID = 
"cos-access-key-id";
+  /** The static secret access key (a.k.a. SecretKey in Tencent Cloud) used to 
access COS data. */
+  public static final String GRAVITINO_COS_STATIC_SECRET_ACCESS_KEY = 
"cos-secret-access-key";
+
+  private String accessKeyId;
+  private String secretAccessKey;
+
+  /**
+   * Constructs an instance of {@link COSSecretKeyCredential} with the static 
COS access key ID and
+   * secret access key.
+   *
+   * @param accessKeyId The COS static access key ID.
+   * @param secretAccessKey The COS static secret access key.
+   */
+  public COSSecretKeyCredential(String accessKeyId, String secretAccessKey) {
+    validate(accessKeyId, secretAccessKey, 0);
+    this.accessKeyId = accessKeyId;
+    this.secretAccessKey = secretAccessKey;
+  }
+
+  /**
+   * This is the constructor that is used by credential factory to create an 
instance of credential
+   * according to the credential information.
+   */
+  public COSSecretKeyCredential() {}
+
+  @Override
+  public String credentialType() {
+    return COS_SECRET_KEY_CREDENTIAL_TYPE;
+  }
+
+  @Override
+  public long expireTimeInMs() {
+    return 0;
+  }
+
+  @Override
+  public Map<String, String> credentialInfo() {
+    return (new ImmutableMap.Builder<String, String>())
+        .put(GRAVITINO_COS_STATIC_ACCESS_KEY_ID, accessKeyId)
+        .put(GRAVITINO_COS_STATIC_SECRET_ACCESS_KEY, secretAccessKey)
+        .build();
+  }
+
+  /**
+   * Initialize the credential with the credential information.
+   *
+   * <p>This method is invoked to deserialize the credential in client side.
+   *
+   * @param credentialInfo The credential information from {@link 
#credentialInfo}.
+   * @param expireTimeInMs The expire-time from {@link #expireTimeInMs()}.
+   */
+  @Override
+  public void initialize(Map<String, String> credentialInfo, long 
expireTimeInMs) {
+    String accessKeyId = 
credentialInfo.get(GRAVITINO_COS_STATIC_ACCESS_KEY_ID);
+    String secretAccessKey = 
credentialInfo.get(GRAVITINO_COS_STATIC_SECRET_ACCESS_KEY);
+    validate(accessKeyId, secretAccessKey, expireTimeInMs);
+    this.accessKeyId = accessKeyId;
+    this.secretAccessKey = secretAccessKey;
+  }
+
+  /**
+   * Get COS static access key ID.
+   *
+   * @return The COS access key ID.
+   */
+  public String accessKeyId() {
+    return accessKeyId;
+  }
+
+  /**
+   * Get COS static secret access key.
+   *
+   * @return The COS secret access key.
+   */
+  public String secretAccessKey() {
+    return secretAccessKey;
+  }
+
+  private void validate(String accessKeyId, String secretAccessKey, long 
expireTimeInMs) {
+    Preconditions.checkArgument(
+        StringUtils.isNotBlank(accessKeyId), "COS access key ID should not be 
empty");
+    Preconditions.checkArgument(
+        StringUtils.isNotBlank(secretAccessKey), "COS secret access key should 
not be empty");
+    Preconditions.checkArgument(
+        expireTimeInMs == 0, "The expiration time of COSSecretKeyCredential is 
not 0");
+  }
+}
diff --git 
a/api/src/main/resources/META-INF/services/org.apache.gravitino.credential.Credential
 
b/api/src/main/resources/META-INF/services/org.apache.gravitino.credential.Credential
index b940abd0b5..3c5672c6cf 100644
--- 
a/api/src/main/resources/META-INF/services/org.apache.gravitino.credential.Credential
+++ 
b/api/src/main/resources/META-INF/services/org.apache.gravitino.credential.Credential
@@ -22,6 +22,7 @@ org.apache.gravitino.credential.S3SecretKeyCredential
 org.apache.gravitino.credential.GCSTokenCredential
 org.apache.gravitino.credential.OSSTokenCredential
 org.apache.gravitino.credential.OSSSecretKeyCredential
+org.apache.gravitino.credential.COSSecretKeyCredential
 org.apache.gravitino.credential.ADLSTokenCredential
 org.apache.gravitino.credential.AzureAccountKeyCredential
 org.apache.gravitino.credential.AwsIrsaCredential
diff --git a/bundles/tencent-bundle/build.gradle.kts 
b/bundles/tencent-bundle/build.gradle.kts
new file mode 100644
index 0000000000..6e421e0a2b
--- /dev/null
+++ b/bundles/tencent-bundle/build.gradle.kts
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
+
+plugins {
+  `maven-publish`
+  id("java")
+  alias(libs.plugins.shadow)
+}
+
+dependencies {
+  implementation(project(":bundles:tencent"))
+
+  implementation(libs.commons.collections3)
+  implementation(libs.commons.io)
+  implementation(libs.hadoop3.client.api)
+  implementation(libs.hadoop3.client.runtime)
+  implementation(libs.hadoop3.cos)
+  implementation(libs.httpclient)
+}
+
+tasks.withType(ShadowJar::class.java) {
+  isZip64 = true
+  configurations = listOf(project.configurations.runtimeClasspath.get())
+  archiveClassifier.set("")
+  mergeServiceFiles()
+
+  dependencies {
+    exclude(dependency("org.slf4j:slf4j-api"))
+
+    // Exclude Gravitino modules to prevent class duplication and "Split 
Packages" issues.
+    // These modules (api, common, catalogs) are already provided by the 
Gravitino server and gravitino-filesystem-hadoop3-runtime.
+    // Including them here would cause the Relocation rules below to 
incorrectly modify
+    // method signatures (e.g., JsonUtils.anyFieldMapper returning a shaded 
ObjectMapper),
+    // leading to java.lang.NoSuchMethodError at runtime.
+    exclude(project(":api"))
+    exclude(project(":common"))
+    exclude(project(":catalogs:catalog-common"))
+    exclude(project(":catalogs:hadoop-common"))
+  }
+
+  // Relocate dependencies to avoid conflicts.
+  // hadoop-cos (from com.qcloud.cos:hadoop-cos) bundles the qcloud-cos SDK 
and a few common
+  // libraries; relocate them under "org.apache.gravitino.tencent.shaded.*" 
following the
+  // same pattern as the aws/aliyun/azure/gcp bundles.
+  relocate("com.fasterxml.jackson", 
"org.apache.gravitino.tencent.shaded.com.fasterxml.jackson")
+  relocate("com.google", "org.apache.gravitino.tencent.shaded.com.google")
+  relocate("com.qcloud", "org.apache.gravitino.tencent.shaded.com.qcloud")
+  relocate("org.apache.commons", 
"org.apache.gravitino.tencent.shaded.org.apache.commons")
+  relocate("org.apache.http", 
"org.apache.gravitino.tencent.shaded.org.apache.http")
+  relocate("org.checkerframework", 
"org.apache.gravitino.tencent.shaded.org.checkerframework")
+  relocate("org.jacoco.agent.rt", 
"org.apache.gravitino.tencent.shaded.org.jacoco.agent.rt")
+}
+
+tasks.jar {
+  dependsOn(tasks.named("shadowJar"))
+  archiveClassifier.set("empty")
+}
+
+tasks.compileJava {
+  dependsOn(":catalogs:catalog-fileset:runtimeJars")
+}
diff --git a/bundles/tencent/build.gradle.kts b/bundles/tencent/build.gradle.kts
new file mode 100644
index 0000000000..98aa70196d
--- /dev/null
+++ b/bundles/tencent/build.gradle.kts
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+plugins {
+  `maven-publish`
+  id("java")
+}
+
+dependencies {
+  implementation(project(":api")) {
+    exclude("*")
+  }
+  implementation(project(":catalogs:catalog-common")) {
+    exclude("*")
+  }
+  implementation(project(":catalogs:hadoop-common")) {
+    exclude("*")
+  }
+  implementation(project(":common")) {
+    exclude("*")
+  }
+
+  implementation(libs.commons.lang3)
+  implementation(libs.guava)
+  implementation(libs.jackson.databind)
+
+  compileOnly(libs.hadoop3.client.api)
+  compileOnly(libs.hadoop3.cos)
+  compileOnly(libs.lombok)
+
+  testImplementation(libs.hadoop3.client.api)
+  testImplementation(libs.hadoop3.client.runtime)
+  testImplementation(libs.hadoop3.cos)
+  testImplementation(libs.junit.jupiter.api)
+  testImplementation(libs.junit.jupiter.params)
+  testRuntimeOnly(libs.junit.jupiter.engine)
+  testRuntimeOnly(libs.bundles.log4j)
+}
+
+tasks.compileJava {
+  dependsOn(":catalogs:catalog-fileset:runtimeJars")
+}
diff --git 
a/bundles/tencent/src/main/java/org/apache/gravitino/cos/credential/COSSecretKeyProvider.java
 
b/bundles/tencent/src/main/java/org/apache/gravitino/cos/credential/COSSecretKeyProvider.java
new file mode 100644
index 0000000000..2abc609134
--- /dev/null
+++ 
b/bundles/tencent/src/main/java/org/apache/gravitino/cos/credential/COSSecretKeyProvider.java
@@ -0,0 +1,60 @@
+/*
+ *  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.cos.credential;
+
+import java.util.Map;
+import org.apache.gravitino.credential.COSSecretKeyCredential;
+import org.apache.gravitino.credential.Credential;
+import org.apache.gravitino.credential.CredentialContext;
+import org.apache.gravitino.credential.CredentialProvider;
+import org.apache.gravitino.credential.config.COSCredentialConfig;
+
+/** Generate COS access key and secret key to access Tencent Cloud COS data. */
+public class COSSecretKeyProvider implements CredentialProvider {
+
+  private String accessKey;
+  private String secretKey;
+
+  @Override
+  public void initialize(Map<String, String> properties) {
+    COSCredentialConfig cosCredentialConfig = new 
COSCredentialConfig(properties);
+    this.accessKey = cosCredentialConfig.accessKeyID();
+    this.secretKey = cosCredentialConfig.secretAccessKey();
+  }
+
+  @Override
+  public void close() {}
+
+  @Override
+  public boolean supportsScheme(String scheme) {
+    // hadoop-cos exposes the `cosn://` scheme, matching 
`COSFileSystemProvider#scheme()`.
+    return "cosn".equalsIgnoreCase(scheme);
+  }
+
+  @Override
+  public String credentialType() {
+    return COSSecretKeyCredential.COS_SECRET_KEY_CREDENTIAL_TYPE;
+  }
+
+  @Override
+  public Credential getCredential(CredentialContext context) {
+    return new COSSecretKeyCredential(accessKey, secretKey);
+  }
+}
diff --git 
a/bundles/tencent/src/main/java/org/apache/gravitino/cos/fs/COSCredentialsProvider.java
 
b/bundles/tencent/src/main/java/org/apache/gravitino/cos/fs/COSCredentialsProvider.java
new file mode 100644
index 0000000000..038271ffcf
--- /dev/null
+++ 
b/bundles/tencent/src/main/java/org/apache/gravitino/cos/fs/COSCredentialsProvider.java
@@ -0,0 +1,91 @@
+/*
+ *  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.cos.fs;
+
+import com.qcloud.cos.auth.BasicCOSCredentials;
+import com.qcloud.cos.auth.COSCredentials;
+import java.net.URI;
+import org.apache.gravitino.catalog.hadoop.fs.FileSystemUtils;
+import 
org.apache.gravitino.catalog.hadoop.fs.GravitinoFileSystemCredentialsProvider;
+import org.apache.gravitino.credential.COSSecretKeyCredential;
+import org.apache.gravitino.credential.Credential;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.auth.AbstractCOSCredentialProvider;
+
+/**
+ * Hadoop-COS credential provider that pulls vended credentials out of 
Gravitino and feeds them to
+ * the underlying {@code com.qcloud.cos.auth.COSCredentialsProvider} contract. 
PR-A only handles
+ * static secret-key credentials; STS / token credentials will be added by a 
follow-up PR.
+ */
+public class COSCredentialsProvider extends AbstractCOSCredentialProvider {
+
+  private final GravitinoFileSystemCredentialsProvider 
gravitinoFileSystemCredentialsProvider;
+  private volatile COSCredentials basicCredentials;
+  private volatile long expirationTime = Long.MAX_VALUE;
+  private static final double EXPIRATION_TIME_FACTOR = 0.5D;
+
+  public COSCredentialsProvider(URI uri, Configuration conf) {
+    super(uri, conf);
+    this.gravitinoFileSystemCredentialsProvider = 
FileSystemUtils.getGvfsCredentialProvider(conf);
+  }
+
+  @Override
+  public COSCredentials getCredentials() {
+    if (basicCredentials == null || System.currentTimeMillis() >= 
expirationTime) {
+      synchronized (this) {
+        if (basicCredentials == null || System.currentTimeMillis() >= 
expirationTime) {
+          refresh();
+        }
+      }
+    }
+    return basicCredentials;
+  }
+
+  @Override
+  public void refresh() {
+    Credential[] gravitinoCredentials = 
gravitinoFileSystemCredentialsProvider.getCredentials();
+    Credential credential = 
COSUtils.getSuitableCredential(gravitinoCredentials);
+    if (credential == null) {
+      throw new RuntimeException("No suitable credential for COS found...");
+    }
+
+    if (credential instanceof COSSecretKeyCredential) {
+      COSSecretKeyCredential cosSecretKeyCredential = (COSSecretKeyCredential) 
credential;
+      this.basicCredentials =
+          new BasicCOSCredentials(
+              cosSecretKeyCredential.accessKeyId(), 
cosSecretKeyCredential.secretAccessKey());
+    } else {
+      // Defensive: COSUtils#getSuitableCredential currently only returns
+      // COSSecretKeyCredential, but a follow-up PR will add STS / token 
support. Failing fast
+      // here avoids silently leaving {@link #basicCredentials} stale (or 
null) if a new
+      // credential type is wired into the selector without updating this 
branch.
+      throw new RuntimeException(
+          "Unsupported credential type for COS: " + 
credential.getClass().getName());
+    }
+
+    if (credential.expireTimeInMs() > 0) {
+      this.expirationTime =
+          System.currentTimeMillis()
+              + (long)
+                  ((credential.expireTimeInMs() - System.currentTimeMillis())
+                      * EXPIRATION_TIME_FACTOR);
+    }
+  }
+}
diff --git 
a/bundles/tencent/src/main/java/org/apache/gravitino/cos/fs/COSFileSystemProvider.java
 
b/bundles/tencent/src/main/java/org/apache/gravitino/cos/fs/COSFileSystemProvider.java
new file mode 100644
index 0000000000..cdc430f27b
--- /dev/null
+++ 
b/bundles/tencent/src/main/java/org/apache/gravitino/cos/fs/COSFileSystemProvider.java
@@ -0,0 +1,128 @@
+/*
+ * 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.cos.fs;
+
+import static 
org.apache.gravitino.catalog.hadoop.fs.Constants.COS_CONNECTION_TIMEOUT_KEY;
+import static 
org.apache.gravitino.catalog.hadoop.fs.Constants.COS_MAX_RETRIES_KEY;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import java.io.IOException;
+import java.util.Map;
+import org.apache.gravitino.catalog.hadoop.fs.FileSystemProvider;
+import org.apache.gravitino.catalog.hadoop.fs.FileSystemUtils;
+import org.apache.gravitino.catalog.hadoop.fs.SupportsCredentialVending;
+import org.apache.gravitino.credential.COSSecretKeyCredential;
+import org.apache.gravitino.credential.Credential;
+import org.apache.gravitino.storage.COSProperties;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.CosFileSystem;
+import org.apache.hadoop.fs.CosNConfigKeys;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+
+public class COSFileSystemProvider implements FileSystemProvider, 
SupportsCredentialVending {
+
+  private static final String COS_FILESYSTEM_IMPL = "fs.cosn.impl";
+
+  // Default connection timeout (ms) used when caller does not specify one. 
Aligns with the
+  // OSS bundle's "fail fast in tests" defaults rather than hadoop-cos's own 
10s default.
+  @VisibleForTesting static final String DEFAULT_COS_CONNECTION_TIMEOUT_MS = 
"5000";
+
+  // Default max retries used when caller does not specify one. hadoop-cos's 
own default is 200,
+  // which is too aggressive for non-production usage.
+  @VisibleForTesting static final String DEFAULT_COS_MAX_RETRIES = "2";
+
+  // Mapping from Gravitino property keys to the corresponding hadoop-cos
+  // (CosNConfigKeys) keys. Mirrors 
OSSFileSystemProvider.GRAVITINO_KEY_TO_OSS_HADOOP_KEY.
+  @VisibleForTesting
+  public static final Map<String, String> GRAVITINO_KEY_TO_COS_HADOOP_KEY =
+      ImmutableMap.of(
+          COSProperties.GRAVITINO_COS_REGION, CosNConfigKeys.COSN_REGION_KEY,
+          COSProperties.GRAVITINO_COS_ENDPOINT, 
CosNConfigKeys.COSN_ENDPOINT_SUFFIX_KEY,
+          COSProperties.GRAVITINO_COS_ACCESS_KEY_ID, 
CosNConfigKeys.COSN_USERINFO_SECRET_ID_KEY,
+          COSProperties.GRAVITINO_COS_ACCESS_KEY_SECRET,
+              CosNConfigKeys.COSN_USERINFO_SECRET_KEY_KEY);
+
+  @Override
+  public FileSystem getFileSystem(Path path, Map<String, String> config) 
throws IOException {
+    Map<String, String> hadoopConfMap =
+        FileSystemUtils.toHadoopConfigMap(config, 
GRAVITINO_KEY_TO_COS_HADOOP_KEY);
+    // hadoop-cos relies on fs.cosn.impl rather than ServiceLoader for 
FileSystem registration.
+    if (!hadoopConfMap.containsKey(COS_FILESYSTEM_IMPL)) {
+      hadoopConfMap.put(COS_FILESYSTEM_IMPL, 
CosFileSystem.class.getCanonicalName());
+    }
+
+    hadoopConfMap = additionalCOSConfig(hadoopConfMap);
+
+    Configuration configuration = 
FileSystemUtils.createCompatibleConfiguration(hadoopConfMap);
+
+    // CosFileSystem does not expose a `newInstance(URI, Configuration)` 
static helper the way
+    // AliyunOSSFileSystem does, so we go through Hadoop's generic 
FileSystem.newInstance which
+    // is the recommended path and honours `fs.cosn.impl`.
+    return FileSystem.newInstance(path.toUri(), configuration);
+  }
+
+  @Override
+  public Map<String, String> getFileSystemCredentialConf(Credential[] 
credentials) {
+    Credential credential = COSUtils.getSuitableCredential(credentials);
+    Map<String, String> result = Maps.newHashMap();
+    if (credential instanceof COSSecretKeyCredential) {
+      result.put(
+          CosNConfigKeys.COSN_CREDENTIALS_PROVIDER,
+          COSCredentialsProvider.class.getCanonicalName());
+    }
+
+    return result;
+  }
+
+  @Override
+  public String scheme() {
+    // hadoop-cos uses the `cosn://` scheme. We expose the same scheme to 
Gravitino users so
+    // existing Hadoop tooling keeps working unchanged.
+    return "cosn";
+  }
+
+  @Override
+  public String name() {
+    return "cos";
+  }
+
+  /**
+   * Add additional COS configurations for better performance and reliability 
in test/dev
+   * environments. Production deployments are free to override these via 
catalog properties.
+   *
+   * @param configs Original configurations
+   * @return Configurations with additional COS settings
+   */
+  private Map<String, String> additionalCOSConfig(Map<String, String> configs) 
{
+    Map<String, String> additionalConfigs = Maps.newHashMap(configs);
+
+    if (!configs.containsKey(COS_CONNECTION_TIMEOUT_KEY)) {
+      additionalConfigs.put(COS_CONNECTION_TIMEOUT_KEY, 
DEFAULT_COS_CONNECTION_TIMEOUT_MS);
+    }
+
+    if (!configs.containsKey(COS_MAX_RETRIES_KEY)) {
+      additionalConfigs.put(COS_MAX_RETRIES_KEY, DEFAULT_COS_MAX_RETRIES);
+    }
+
+    return ImmutableMap.copyOf(additionalConfigs);
+  }
+}
diff --git 
a/bundles/tencent/src/main/java/org/apache/gravitino/cos/fs/COSUtils.java 
b/bundles/tencent/src/main/java/org/apache/gravitino/cos/fs/COSUtils.java
new file mode 100644
index 0000000000..1a1dfcf664
--- /dev/null
+++ b/bundles/tencent/src/main/java/org/apache/gravitino/cos/fs/COSUtils.java
@@ -0,0 +1,45 @@
+/*
+ *  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.cos.fs;
+
+import org.apache.gravitino.credential.COSSecretKeyCredential;
+import org.apache.gravitino.credential.Credential;
+
+public class COSUtils {
+
+  /**
+   * Get the credential from the credential array. PR-A only ships static 
secret-key support; STS
+   * token credentials will be added by a follow-up PR, at which point this 
helper should mirror
+   * {@code OSSUtils#getSuitableCredential} and prefer dynamic over static 
credentials.
+   *
+   * @param credentials The credential array.
+   * @return A credential. Null if not found.
+   */
+  static Credential getSuitableCredential(Credential[] credentials) {
+    for (Credential credential : credentials) {
+      if (credential instanceof COSSecretKeyCredential) {
+        return credential;
+      }
+    }
+    return null;
+  }
+
+  private COSUtils() {}
+}
diff --git 
a/api/src/main/resources/META-INF/services/org.apache.gravitino.credential.Credential
 
b/bundles/tencent/src/main/resources/META-INF/services/org.apache.gravitino.catalog.hadoop.fs.FileSystemProvider
similarity index 62%
copy from 
api/src/main/resources/META-INF/services/org.apache.gravitino.credential.Credential
copy to 
bundles/tencent/src/main/resources/META-INF/services/org.apache.gravitino.catalog.hadoop.fs.FileSystemProvider
index b940abd0b5..6c5369c6e4 100644
--- 
a/api/src/main/resources/META-INF/services/org.apache.gravitino.credential.Credential
+++ 
b/bundles/tencent/src/main/resources/META-INF/services/org.apache.gravitino.catalog.hadoop.fs.FileSystemProvider
@@ -17,12 +17,4 @@
 # under the License.
 #
 
-org.apache.gravitino.credential.S3TokenCredential
-org.apache.gravitino.credential.S3SecretKeyCredential
-org.apache.gravitino.credential.GCSTokenCredential
-org.apache.gravitino.credential.OSSTokenCredential
-org.apache.gravitino.credential.OSSSecretKeyCredential
-org.apache.gravitino.credential.ADLSTokenCredential
-org.apache.gravitino.credential.AzureAccountKeyCredential
-org.apache.gravitino.credential.AwsIrsaCredential
-org.apache.gravitino.credential.JdbcCredential
+org.apache.gravitino.cos.fs.COSFileSystemProvider
diff --git 
a/api/src/main/resources/META-INF/services/org.apache.gravitino.credential.Credential
 
b/bundles/tencent/src/main/resources/META-INF/services/org.apache.gravitino.credential.CredentialProvider
similarity index 62%
copy from 
api/src/main/resources/META-INF/services/org.apache.gravitino.credential.Credential
copy to 
bundles/tencent/src/main/resources/META-INF/services/org.apache.gravitino.credential.CredentialProvider
index b940abd0b5..8877014411 100644
--- 
a/api/src/main/resources/META-INF/services/org.apache.gravitino.credential.Credential
+++ 
b/bundles/tencent/src/main/resources/META-INF/services/org.apache.gravitino.credential.CredentialProvider
@@ -16,13 +16,4 @@
 # specific language governing permissions and limitations
 # under the License.
 #
-
-org.apache.gravitino.credential.S3TokenCredential
-org.apache.gravitino.credential.S3SecretKeyCredential
-org.apache.gravitino.credential.GCSTokenCredential
-org.apache.gravitino.credential.OSSTokenCredential
-org.apache.gravitino.credential.OSSSecretKeyCredential
-org.apache.gravitino.credential.ADLSTokenCredential
-org.apache.gravitino.credential.AzureAccountKeyCredential
-org.apache.gravitino.credential.AwsIrsaCredential
-org.apache.gravitino.credential.JdbcCredential
+org.apache.gravitino.cos.credential.COSSecretKeyProvider
diff --git 
a/bundles/tencent/src/test/java/org/apache/gravitino/cos/credential/TestCOSCredentialProvider.java
 
b/bundles/tencent/src/test/java/org/apache/gravitino/cos/credential/TestCOSCredentialProvider.java
new file mode 100644
index 0000000000..1a83c3795e
--- /dev/null
+++ 
b/bundles/tencent/src/test/java/org/apache/gravitino/cos/credential/TestCOSCredentialProvider.java
@@ -0,0 +1,63 @@
+/*
+ *  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.cos.credential;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.gravitino.credential.COSSecretKeyCredential;
+import org.apache.gravitino.credential.Credential;
+import org.apache.gravitino.storage.COSProperties;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestCOSCredentialProvider {
+
+  @Test
+  void testCredentialType() {
+    COSSecretKeyProvider provider = new COSSecretKeyProvider();
+    Assertions.assertEquals(
+        COSSecretKeyCredential.COS_SECRET_KEY_CREDENTIAL_TYPE, 
provider.credentialType());
+  }
+
+  @Test
+  void testSupportsScheme() {
+    COSSecretKeyProvider provider = new COSSecretKeyProvider();
+    Assertions.assertTrue(provider.supportsScheme("cosn"));
+    Assertions.assertTrue(provider.supportsScheme("COSN"));
+    Assertions.assertFalse(provider.supportsScheme("cos"));
+    Assertions.assertFalse(provider.supportsScheme("oss"));
+    Assertions.assertFalse(provider.supportsScheme("s3a"));
+  }
+
+  @Test
+  void testInitializeAndGetCredential() {
+    COSSecretKeyProvider provider = new COSSecretKeyProvider();
+    provider.initialize(
+        ImmutableMap.of(
+            COSProperties.GRAVITINO_COS_ACCESS_KEY_ID, "ak",
+            COSProperties.GRAVITINO_COS_ACCESS_KEY_SECRET, "sk"));
+
+    Credential credential = provider.getCredential(null);
+
+    Assertions.assertTrue(credential instanceof COSSecretKeyCredential);
+    COSSecretKeyCredential typed = (COSSecretKeyCredential) credential;
+    Assertions.assertEquals("ak", typed.accessKeyId());
+    Assertions.assertEquals("sk", typed.secretAccessKey());
+  }
+}
diff --git 
a/bundles/tencent/src/test/java/org/apache/gravitino/cos/fs/TestCOSCredentialsProvider.java
 
b/bundles/tencent/src/test/java/org/apache/gravitino/cos/fs/TestCOSCredentialsProvider.java
new file mode 100644
index 0000000000..f1b1244bec
--- /dev/null
+++ 
b/bundles/tencent/src/test/java/org/apache/gravitino/cos/fs/TestCOSCredentialsProvider.java
@@ -0,0 +1,154 @@
+/*
+ *  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.cos.fs;
+
+import com.qcloud.cos.auth.BasicCOSCredentials;
+import com.qcloud.cos.auth.COSCredentials;
+import java.net.URI;
+import java.util.concurrent.atomic.AtomicInteger;
+import 
org.apache.gravitino.catalog.hadoop.fs.GravitinoFileSystemCredentialsProvider;
+import org.apache.gravitino.credential.COSSecretKeyCredential;
+import org.apache.gravitino.credential.Credential;
+import org.apache.hadoop.conf.Configuration;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link COSCredentialsProvider}, the Hadoop-side bridge that 
turns Gravitino-vended
+ * credentials into {@code com.qcloud.cos.auth.COSCredentials} instances. The 
stub {@link
+ * StubGravitinoFileSystemCredentialsProvider} below is wired via {@code
+ * fs.gvfs.credential.provider} so that {@code 
FileSystemUtils.getGvfsCredentialProvider} can
+ * reflectively instantiate it during the {@link COSCredentialsProvider} 
constructor.
+ */
+public class TestCOSCredentialsProvider {
+
+  private static final URI COS_URI = 
URI.create("cosn://test-bucket-1250000000/");
+
+  @BeforeEach
+  void resetStub() {
+    StubGravitinoFileSystemCredentialsProvider.reset();
+  }
+
+  @AfterEach
+  void clearStub() {
+    StubGravitinoFileSystemCredentialsProvider.reset();
+  }
+
+  @Test
+  void testRefreshWithSecretKeyCredential() {
+    StubGravitinoFileSystemCredentialsProvider.nextCredentials =
+        new Credential[] {new COSSecretKeyCredential("test-ak", "test-sk")};
+
+    COSCredentialsProvider provider = new COSCredentialsProvider(COS_URI, 
newStubConf());
+
+    COSCredentials credentials = provider.getCredentials();
+
+    Assertions.assertNotNull(credentials);
+    Assertions.assertTrue(credentials instanceof BasicCOSCredentials);
+    Assertions.assertEquals("test-ak", credentials.getCOSAccessKeyId());
+    Assertions.assertEquals("test-sk", credentials.getCOSSecretKey());
+  }
+
+  @Test
+  void testRefreshThrowsWhenNoSuitableCredential() {
+    // No COSSecretKeyCredential in the array -> 
COSUtils#getSuitableCredential returns null
+    // -> COSCredentialsProvider#refresh throws.
+    StubGravitinoFileSystemCredentialsProvider.nextCredentials = new 
Credential[] {};
+
+    COSCredentialsProvider provider = new COSCredentialsProvider(COS_URI, 
newStubConf());
+
+    RuntimeException ex = Assertions.assertThrows(RuntimeException.class, 
provider::getCredentials);
+    Assertions.assertTrue(
+        ex.getMessage() != null && ex.getMessage().contains("No suitable 
credential"),
+        "Expected message about no suitable credential, but got: " + 
ex.getMessage());
+  }
+
+  @Test
+  void testGetCredentialsIsCachedForNonExpiringCredential() {
+    // COSSecretKeyCredential#expireTimeInMs() returns 0, so expirationTime 
stays at
+    // Long.MAX_VALUE and subsequent getCredentials() calls must return the 
cached
+    // BasicCOSCredentials without re-asking the upstream provider.
+    StubGravitinoFileSystemCredentialsProvider.nextCredentials =
+        new Credential[] {new COSSecretKeyCredential("ak1", "sk1")};
+
+    COSCredentialsProvider provider = new COSCredentialsProvider(COS_URI, 
newStubConf());
+
+    COSCredentials first = provider.getCredentials();
+    COSCredentials second = provider.getCredentials();
+    COSCredentials third = provider.getCredentials();
+
+    Assertions.assertSame(first, second);
+    Assertions.assertSame(second, third);
+    Assertions.assertEquals(
+        1,
+        StubGravitinoFileSystemCredentialsProvider.callCount.get(),
+        "Static, non-expiring credentials should only be fetched from the 
upstream provider once");
+  }
+
+  private static Configuration newStubConf() {
+    Configuration conf = new Configuration(false);
+    conf.set(
+        GravitinoFileSystemCredentialsProvider.GVFS_CREDENTIAL_PROVIDER,
+        StubGravitinoFileSystemCredentialsProvider.class.getName());
+    return conf;
+  }
+
+  /**
+   * Test stub for {@link GravitinoFileSystemCredentialsProvider}. Must be 
{@code public} with a
+   * public no-arg constructor because {@code 
FileSystemUtils#getGvfsCredentialProvider}
+   * instantiates it reflectively.
+   *
+   * <p>Tests communicate with the stub through the {@link #nextCredentials} 
static field, because
+   * the reflective construction prevents passing values via the constructor.
+   */
+  public static final class StubGravitinoFileSystemCredentialsProvider
+      implements GravitinoFileSystemCredentialsProvider {
+
+    static volatile Credential[] nextCredentials = new Credential[] {};
+    static final AtomicInteger callCount = new AtomicInteger(0);
+
+    private Configuration conf;
+
+    public StubGravitinoFileSystemCredentialsProvider() {}
+
+    static void reset() {
+      nextCredentials = new Credential[] {};
+      callCount.set(0);
+    }
+
+    @Override
+    public Credential[] getCredentials() {
+      callCount.incrementAndGet();
+      return nextCredentials;
+    }
+
+    @Override
+    public void setConf(Configuration conf) {
+      this.conf = conf;
+    }
+
+    @Override
+    public Configuration getConf() {
+      return conf;
+    }
+  }
+}
diff --git 
a/bundles/tencent/src/test/java/org/apache/gravitino/cos/fs/TestCOSFileSystemProvider.java
 
b/bundles/tencent/src/test/java/org/apache/gravitino/cos/fs/TestCOSFileSystemProvider.java
new file mode 100644
index 0000000000..399e782754
--- /dev/null
+++ 
b/bundles/tencent/src/test/java/org/apache/gravitino/cos/fs/TestCOSFileSystemProvider.java
@@ -0,0 +1,92 @@
+/*
+ *  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.cos.fs;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.gravitino.credential.COSSecretKeyCredential;
+import org.apache.gravitino.credential.Credential;
+import org.apache.gravitino.storage.COSProperties;
+import org.apache.hadoop.fs.CosNConfigKeys;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestCOSFileSystemProvider {
+
+  @Test
+  void testSchemeAndName() {
+    COSFileSystemProvider provider = new COSFileSystemProvider();
+    Assertions.assertEquals("cosn", provider.scheme());
+    Assertions.assertEquals("cos", provider.name());
+  }
+
+  @Test
+  void testGravitinoToHadoopKeyMapping() {
+    Map<String, String> mapping = 
COSFileSystemProvider.GRAVITINO_KEY_TO_COS_HADOOP_KEY;
+
+    Assertions.assertEquals(
+        CosNConfigKeys.COSN_REGION_KEY, 
mapping.get(COSProperties.GRAVITINO_COS_REGION));
+    Assertions.assertEquals(
+        CosNConfigKeys.COSN_ENDPOINT_SUFFIX_KEY, 
mapping.get(COSProperties.GRAVITINO_COS_ENDPOINT));
+    Assertions.assertEquals(
+        CosNConfigKeys.COSN_USERINFO_SECRET_ID_KEY,
+        mapping.get(COSProperties.GRAVITINO_COS_ACCESS_KEY_ID));
+    Assertions.assertEquals(
+        CosNConfigKeys.COSN_USERINFO_SECRET_KEY_KEY,
+        mapping.get(COSProperties.GRAVITINO_COS_ACCESS_KEY_SECRET));
+  }
+
+  @Test
+  void testGetFileSystemCredentialConfWithSecretKey() {
+    COSFileSystemProvider provider = new COSFileSystemProvider();
+    Credential[] credentials = new Credential[] {new 
COSSecretKeyCredential("ak", "sk")};
+
+    Map<String, String> conf = 
provider.getFileSystemCredentialConf(credentials);
+
+    Assertions.assertEquals(
+        COSCredentialsProvider.class.getCanonicalName(),
+        conf.get(CosNConfigKeys.COSN_CREDENTIALS_PROVIDER));
+  }
+
+  @Test
+  void testGetFileSystemCredentialConfWithEmptyCredentials() {
+    COSFileSystemProvider provider = new COSFileSystemProvider();
+
+    Map<String, String> conf = provider.getFileSystemCredentialConf(new 
Credential[0]);
+
+    Assertions.assertTrue(
+        conf.isEmpty(),
+        "No credentials should yield an empty conf map (no credentials 
provider injected)");
+  }
+
+  @Test
+  void testKeyMappingDoesNotLeakUnknownProperties() {
+    // Sanity: random non-COS properties must not appear in the 
Gravitino->Hadoop mapping.
+    Map<String, String> mapping = 
COSFileSystemProvider.GRAVITINO_KEY_TO_COS_HADOOP_KEY;
+    Map<String, String> notMapped = new HashMap<>();
+    notMapped.put("oss-region", "value");
+    notMapped.put("s3-region", "value");
+    for (String key : notMapped.keySet()) {
+      Assertions.assertFalse(
+          mapping.containsKey(key),
+          "Gravitino->Hadoop COS mapping should not contain unrelated key: " + 
key);
+    }
+  }
+}
diff --git 
a/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/config/COSCredentialConfig.java
 
b/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/config/COSCredentialConfig.java
new file mode 100644
index 0000000000..ab9b2b0faf
--- /dev/null
+++ 
b/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/config/COSCredentialConfig.java
@@ -0,0 +1,79 @@
+/*
+ *  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.credential.config;
+
+import java.util.Map;
+import javax.validation.constraints.NotNull;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.config.ConfigBuilder;
+import org.apache.gravitino.config.ConfigConstants;
+import org.apache.gravitino.config.ConfigEntry;
+import org.apache.gravitino.storage.COSProperties;
+
+/**
+ * Slim credential config for Tencent Cloud COS, covering only the static 
secret-key path. STS /
+ * token-related entries (role arn, token expire) will be added by a follow-up 
PR that introduces
+ * the dynamic credential vending support.
+ */
+public class COSCredentialConfig extends Config {
+
+  public static final ConfigEntry<String> COS_REGION =
+      new ConfigBuilder(COSProperties.GRAVITINO_COS_REGION)
+          .doc("The region of the Tencent Cloud COS service")
+          .version(ConfigConstants.VERSION_2_0_0)
+          .stringConf()
+          .create();
+
+  public static final ConfigEntry<String> COS_ACCESS_KEY_ID =
+      new ConfigBuilder(COSProperties.GRAVITINO_COS_ACCESS_KEY_ID)
+          .doc("The static access key ID (Tencent Cloud SecretId) used to 
access COS data")
+          .version(ConfigConstants.VERSION_2_0_0)
+          .stringConf()
+          .checkValue(StringUtils::isNotBlank, 
ConfigConstants.NOT_BLANK_ERROR_MSG)
+          .create();
+
+  public static final ConfigEntry<String> COS_SECRET_ACCESS_KEY =
+      new ConfigBuilder(COSProperties.GRAVITINO_COS_ACCESS_KEY_SECRET)
+          .doc("The static secret access key (Tencent Cloud SecretKey) used to 
access COS data")
+          .version(ConfigConstants.VERSION_2_0_0)
+          .stringConf()
+          .checkValue(StringUtils::isNotBlank, 
ConfigConstants.NOT_BLANK_ERROR_MSG)
+          .create();
+
+  public COSCredentialConfig(Map<String, String> properties) {
+    super(false);
+    loadFromMap(properties, k -> true);
+  }
+
+  public String region() {
+    return this.get(COS_REGION);
+  }
+
+  @NotNull
+  public String accessKeyID() {
+    return this.get(COS_ACCESS_KEY_ID);
+  }
+
+  @NotNull
+  public String secretAccessKey() {
+    return this.get(COS_SECRET_ACCESS_KEY);
+  }
+}
diff --git 
a/catalogs/catalog-common/src/main/java/org/apache/gravitino/storage/COSProperties.java
 
b/catalogs/catalog-common/src/main/java/org/apache/gravitino/storage/COSProperties.java
new file mode 100644
index 0000000000..7326ebc335
--- /dev/null
+++ 
b/catalogs/catalog-common/src/main/java/org/apache/gravitino/storage/COSProperties.java
@@ -0,0 +1,34 @@
+/*
+ * 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.storage;
+
+// Properties for Tencent Cloud COS.
+public class COSProperties {
+
+  // The region of Tencent Cloud COS, e.g. "ap-guangzhou".
+  public static final String GRAVITINO_COS_REGION = "cos-region";
+  // The endpoint of Tencent Cloud COS (optional, normally derived from 
region).
+  public static final String GRAVITINO_COS_ENDPOINT = "cos-endpoint";
+  // The static access key ID (Tencent Cloud SecretId) used to access COS data.
+  public static final String GRAVITINO_COS_ACCESS_KEY_ID = "cos-access-key-id";
+  // The static secret access key (Tencent Cloud SecretKey) used to access COS 
data.
+  public static final String GRAVITINO_COS_ACCESS_KEY_SECRET = 
"cos-secret-access-key";
+
+  private COSProperties() {}
+}
diff --git a/catalogs/catalog-fileset/build.gradle.kts 
b/catalogs/catalog-fileset/build.gradle.kts
index 0ed158f61a..ced821091f 100644
--- a/catalogs/catalog-fileset/build.gradle.kts
+++ b/catalogs/catalog-fileset/build.gradle.kts
@@ -58,6 +58,7 @@ dependencies {
   testImplementation(project(":bundles:aws-bundle", configuration = "shadow"))
   testImplementation(project(":bundles:azure-bundle", configuration = 
"shadow"))
   testImplementation(project(":bundles:gcp-bundle", configuration = "shadow"))
+  testImplementation(project(":bundles:tencent-bundle", configuration = 
"shadow"))
   testImplementation(project(":clients:client-java"))
   testImplementation(project(":common"))
   testImplementation(project(":core"))
@@ -146,6 +147,7 @@ tasks.test {
   dependsOn(":bundles:aws-bundle:jar")
   dependsOn(":bundles:aliyun-bundle:jar")
   dependsOn(":bundles:gcp-bundle:jar")
+  dependsOn(":bundles:tencent-bundle:jar")
 }
 
 tasks.getByName("generateMetadataFileForMavenJavaPublication") {
diff --git 
a/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/integration/test/FilesetCOSCatalogIT.java
 
b/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/integration/test/FilesetCOSCatalogIT.java
new file mode 100644
index 0000000000..813d8cd441
--- /dev/null
+++ 
b/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/integration/test/FilesetCOSCatalogIT.java
@@ -0,0 +1,207 @@
+/*
+ * 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.fileset.integration.test;
+
+import static 
org.apache.gravitino.catalog.fileset.FilesetCatalogPropertiesMetadata.FILESYSTEM_PROVIDERS;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Map;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Schema;
+import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.integration.test.util.GravitinoITUtils;
+import org.apache.gravitino.storage.COSProperties;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIf;
+import org.junit.platform.commons.util.StringUtils;
+
+/**
+ * Integration test for the fileset catalog backed by Tencent Cloud COS. It is 
disabled unless the
+ * required environment variables (COS_*) are present, mirroring the OSS / AWS 
counterparts.
+ */
+@EnabledIf(value = "cosIsConfigured", disabledReason = "Tencent Cloud COS is 
not configured.")
+public class FilesetCOSCatalogIT extends FilesetCatalogIT {
+
+  public static final String BUCKET_NAME = System.getenv("COS_BUCKET_NAME");
+  public static final String COS_ACCESS_KEY = 
System.getenv("COS_ACCESS_KEY_ID");
+  public static final String COS_SECRET_KEY = 
System.getenv("COS_SECRET_ACCESS_KEY");
+  public static final String COS_REGION = System.getenv("COS_REGION");
+  // Optional: caller may override the endpoint suffix (e.g. for cos-internal 
endpoints).
+  public static final String COS_ENDPOINT = System.getenv("COS_ENDPOINT");
+
+  @VisibleForTesting
+  public void startIntegrationTest() throws Exception {}
+
+  @BeforeAll
+  public void setup() throws IOException {
+    copyBundleJarsToHadoop("tencent-bundle");
+
+    try {
+      super.startIntegrationTest();
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to start integration test", e);
+    }
+
+    metalakeName = GravitinoITUtils.genRandomName("CatalogFilesetIT_metalake");
+    catalogName = GravitinoITUtils.genRandomName("CatalogFilesetIT_catalog");
+    schemaName = GravitinoITUtils.genRandomName(SCHEMA_PREFIX);
+    Configuration conf = new Configuration();
+
+    conf.set("fs.cosn.userinfo.secretId", COS_ACCESS_KEY);
+    conf.set("fs.cosn.userinfo.secretKey", COS_SECRET_KEY);
+    conf.set("fs.cosn.bucket.region", COS_REGION);
+    if (StringUtils.isNotBlank(COS_ENDPOINT)) {
+      conf.set("fs.cosn.bucket.endpoint_suffix", COS_ENDPOINT);
+    }
+    conf.set("fs.cosn.impl", "org.apache.hadoop.fs.CosFileSystem");
+    fileSystem = FileSystem.get(URI.create(String.format("cosn://%s", 
BUCKET_NAME)), conf);
+
+    createMetalake();
+    createCatalog();
+    createSchema();
+  }
+
+  @AfterAll
+  public void stop() throws IOException {
+    super.stop();
+  }
+
+  protected String defaultBaseLocation() {
+    if (defaultBaseLocation == null) {
+      try {
+        Path bucket =
+            new Path(
+                String.format(
+                    "cosn://%s/%s",
+                    BUCKET_NAME, 
GravitinoITUtils.genRandomName("CatalogFilesetIT")));
+        if (!fileSystem.exists(bucket)) {
+          fileSystem.mkdirs(bucket);
+        }
+
+        defaultBaseLocation = bucket.toString();
+      } catch (IOException e) {
+        throw new RuntimeException("Failed to create default base location", 
e);
+      }
+    }
+
+    return defaultBaseLocation;
+  }
+
+  protected void createCatalog() {
+    Map<String, String> map = Maps.newHashMap();
+    map.put(COSProperties.GRAVITINO_COS_ACCESS_KEY_ID, COS_ACCESS_KEY);
+    map.put(COSProperties.GRAVITINO_COS_ACCESS_KEY_SECRET, COS_SECRET_KEY);
+    map.put(COSProperties.GRAVITINO_COS_REGION, COS_REGION);
+    if (StringUtils.isNotBlank(COS_ENDPOINT)) {
+      map.put(COSProperties.GRAVITINO_COS_ENDPOINT, COS_ENDPOINT);
+    }
+    map.put(FILESYSTEM_PROVIDERS, "cos");
+
+    metalake.createCatalog(catalogName, Catalog.Type.FILESET, provider, 
"comment", map);
+
+    catalog = metalake.loadCatalog(catalogName);
+  }
+
+  protected String generateLocation(String filesetName) {
+    return String.format("%s/%s", defaultBaseLocation, filesetName);
+  }
+
+  @Test
+  public void testCreateSchemaAndFilesetWithSpecialLocation() {
+    String localCatalogName = GravitinoITUtils.genRandomName("local_catalog");
+
+    String cosLocation = String.format("cosn://%s", BUCKET_NAME);
+    Map<String, String> catalogProps = Maps.newHashMap();
+    catalogProps.put("location", cosLocation);
+    catalogProps.put(COSProperties.GRAVITINO_COS_ACCESS_KEY_ID, 
COS_ACCESS_KEY);
+    catalogProps.put(COSProperties.GRAVITINO_COS_ACCESS_KEY_SECRET, 
COS_SECRET_KEY);
+    catalogProps.put(COSProperties.GRAVITINO_COS_REGION, COS_REGION);
+    if (StringUtils.isNotBlank(COS_ENDPOINT)) {
+      catalogProps.put(COSProperties.GRAVITINO_COS_ENDPOINT, COS_ENDPOINT);
+    }
+    catalogProps.put(FILESYSTEM_PROVIDERS, "cos");
+
+    Catalog localCatalog =
+        metalake.createCatalog(
+            localCatalogName, Catalog.Type.FILESET, provider, "comment", 
catalogProps);
+    Assertions.assertEquals(cosLocation, 
localCatalog.properties().get("location"));
+
+    // Create schema without specifying location.
+    Schema localSchema =
+        localCatalog
+            .asSchemas()
+            .createSchema("local_schema", "comment", ImmutableMap.of("key1", 
"val1"));
+
+    Fileset localFileset =
+        localCatalog
+            .asFilesetCatalog()
+            .createFileset(
+                NameIdentifier.of(localSchema.name(), "local_fileset"),
+                "fileset comment",
+                Fileset.Type.MANAGED,
+                null,
+                ImmutableMap.of("k1", "v1"));
+    Assertions.assertEquals(
+        cosLocation + "/local_schema/local_fileset", 
localFileset.storageLocation());
+
+    // Delete schema
+    localCatalog.asSchemas().dropSchema(localSchema.name(), true);
+
+    // Create schema with specifying location.
+    Map<String, String> schemaProps = ImmutableMap.of("location", cosLocation);
+    Schema localSchema2 =
+        localCatalog.asSchemas().createSchema("local_schema2", "comment", 
schemaProps);
+    Assertions.assertEquals(cosLocation, 
localSchema2.properties().get("location"));
+
+    Fileset localFileset2 =
+        localCatalog
+            .asFilesetCatalog()
+            .createFileset(
+                NameIdentifier.of(localSchema2.name(), "local_fileset2"),
+                "fileset comment",
+                Fileset.Type.MANAGED,
+                null,
+                ImmutableMap.of("k1", "v1"));
+    Assertions.assertEquals(cosLocation + "/local_fileset2", 
localFileset2.storageLocation());
+
+    // Delete schema
+    localCatalog.asSchemas().dropSchema(localSchema2.name(), true);
+
+    // Delete catalog
+    metalake.dropCatalog(localCatalogName, true);
+  }
+
+  protected static boolean cosIsConfigured() {
+    return StringUtils.isNotBlank(System.getenv("COS_ACCESS_KEY_ID"))
+        && StringUtils.isNotBlank(System.getenv("COS_SECRET_ACCESS_KEY"))
+        && StringUtils.isNotBlank(System.getenv("COS_REGION"))
+        && StringUtils.isNotBlank(System.getenv("COS_BUCKET_NAME"));
+  }
+}
diff --git 
a/catalogs/hadoop-common/src/main/java/org/apache/gravitino/catalog/hadoop/fs/Constants.java
 
b/catalogs/hadoop-common/src/main/java/org/apache/gravitino/catalog/hadoop/fs/Constants.java
index 39ef923b39..76edc5b9cc 100644
--- 
a/catalogs/hadoop-common/src/main/java/org/apache/gravitino/catalog/hadoop/fs/Constants.java
+++ 
b/catalogs/hadoop-common/src/main/java/org/apache/gravitino/catalog/hadoop/fs/Constants.java
@@ -67,6 +67,12 @@ public class Constants {
   public static final String OSS_ESTABLISH_TIMEOUT_KEY = 
"fs.oss.connection.establish.timeout";
   public static final String OSS_MAX_ERROR_RETRIES_KEY = 
"fs.oss.attempts.maximum";
 
+  /** Maps to {@code CosNConfigKeys.COSN_CONNECTION_TIMEOUT}. hadoop-cos 
default is 10000 ms. */
+  public static final String COS_CONNECTION_TIMEOUT_KEY = 
"fs.cosn.connection.timeout";
+
+  /** Maps to {@code CosNConfigKeys.COSN_MAX_RETRIES_KEY}. hadoop-cos default 
is 200. */
+  public static final String COS_MAX_RETRIES_KEY = "fs.cosn.maxRetries";
+
   // Azure Blob Storage specific configuration keys, please see: 
AbfsConfiguration
   public static final String ADLS_MAX_RETRIES = 
"fs.azure.io.retry.max.retries";
 
diff --git a/clients/filesystem-hadoop3-runtime/build.gradle.kts 
b/clients/filesystem-hadoop3-runtime/build.gradle.kts
index 2a5b36b79f..28aa61be43 100644
--- a/clients/filesystem-hadoop3-runtime/build.gradle.kts
+++ b/clients/filesystem-hadoop3-runtime/build.gradle.kts
@@ -30,6 +30,7 @@ dependencies {
   implementation(project(":bundles:aws"))
   implementation(project(":bundles:azure"))
   implementation(project(":bundles:gcp"))
+  implementation(project(":bundles:tencent"))
   implementation(project(":clients:filesystem-hadoop3")) {
     exclude(group = "org.slf4j")
   }
diff --git a/clients/filesystem-hadoop3/build.gradle.kts 
b/clients/filesystem-hadoop3/build.gradle.kts
index 76000c6abe..404ea2f8b5 100644
--- a/clients/filesystem-hadoop3/build.gradle.kts
+++ b/clients/filesystem-hadoop3/build.gradle.kts
@@ -54,6 +54,7 @@ dependencies {
   testImplementation(project(":bundles:gcp-bundle", configuration = "shadow"))
   testImplementation(project(":bundles:aliyun-bundle", configuration = 
"shadow"))
   testImplementation(project(":bundles:azure-bundle", configuration = 
"shadow"))
+  testImplementation(project(":bundles:tencent-bundle", configuration = 
"shadow"))
 
   testImplementation(libs.awaitility)
   testImplementation(libs.bundles.jetty)
@@ -109,6 +110,7 @@ tasks.test {
   dependsOn(":bundles:aliyun-bundle:jar")
   dependsOn(":bundles:gcp-bundle:jar")
   dependsOn(":bundles:azure-bundle:jar")
+  dependsOn(":bundles:tencent-bundle:jar")
 }
 
 tasks.javadoc {
diff --git 
a/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/integration/test/GravitinoVirtualFileSystemCOSIT.java
 
b/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/integration/test/GravitinoVirtualFileSystemCOSIT.java
new file mode 100644
index 0000000000..e8880244a0
--- /dev/null
+++ 
b/clients/filesystem-hadoop3/src/test/java/org/apache/gravitino/filesystem/hadoop/integration/test/GravitinoVirtualFileSystemCOSIT.java
@@ -0,0 +1,169 @@
+/*
+ * 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.filesystem.hadoop.integration.test;
+
+import static 
org.apache.gravitino.catalog.fileset.FilesetCatalogPropertiesMetadata.FILESYSTEM_PROVIDERS;
+
+import com.google.common.collect.Maps;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.catalog.hadoop.fs.FileSystemUtils;
+import org.apache.gravitino.cos.fs.COSFileSystemProvider;
+import org.apache.gravitino.integration.test.util.GravitinoITUtils;
+import org.apache.gravitino.storage.COSProperties;
+import org.apache.hadoop.conf.Configuration;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.condition.EnabledIf;
+import org.junit.platform.commons.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * GVFS integration test against a real Tencent Cloud COS bucket. It is 
disabled unless the required
+ * COS_* environment variables are present, mirroring {@link 
GravitinoVirtualFileSystemOSSIT}.
+ */
+@EnabledIf(value = "cosIsConfigured", disabledReason = "COS is not prepared")
+public class GravitinoVirtualFileSystemCOSIT extends 
GravitinoVirtualFileSystemIT {
+  private static final Logger LOG = 
LoggerFactory.getLogger(GravitinoVirtualFileSystemCOSIT.class);
+
+  public static final String BUCKET_NAME = System.getenv("COS_BUCKET_NAME");
+  public static final String COS_ACCESS_KEY = 
System.getenv("COS_ACCESS_KEY_ID");
+  public static final String COS_SECRET_KEY = 
System.getenv("COS_SECRET_ACCESS_KEY");
+  public static final String COS_REGION = System.getenv("COS_REGION");
+  // Optional: caller may override the endpoint suffix (e.g. for cos-internal 
endpoints).
+  public static final String COS_ENDPOINT = System.getenv("COS_ENDPOINT");
+
+  @BeforeAll
+  public void startIntegrationTest() {
+    // Do nothing
+  }
+
+  @BeforeAll
+  public void startUp() throws Exception {
+    copyBundleJarsToHadoop("tencent-bundle");
+    // Need to download jars to gravitino server
+    super.startIntegrationTest();
+
+    // Default of fs.cosn.block.size is 128MB.
+    defaultBlockSize = 128 * 1024 * 1024;
+
+    // The default replication factor is 1.
+    defaultReplication = 1;
+
+    metalakeName = GravitinoITUtils.genRandomName("gvfs_it_metalake");
+    catalogName = GravitinoITUtils.genRandomName("catalog");
+    schemaName = GravitinoITUtils.genRandomName("schema");
+
+    Assertions.assertFalse(client.metalakeExists(metalakeName));
+    metalake = client.createMetalake(metalakeName, "metalake comment", 
Collections.emptyMap());
+    Assertions.assertTrue(client.metalakeExists(metalakeName));
+
+    Map<String, String> properties = Maps.newHashMap();
+    properties.put(FILESYSTEM_PROVIDERS, "cos");
+    properties.put(COSProperties.GRAVITINO_COS_ACCESS_KEY_ID, COS_ACCESS_KEY);
+    properties.put(COSProperties.GRAVITINO_COS_ACCESS_KEY_SECRET, 
COS_SECRET_KEY);
+    properties.put(COSProperties.GRAVITINO_COS_REGION, COS_REGION);
+    if (StringUtils.isNotBlank(COS_ENDPOINT)) {
+      properties.put(COSProperties.GRAVITINO_COS_ENDPOINT, COS_ENDPOINT);
+    }
+
+    Catalog catalog =
+        metalake.createCatalog(
+            catalogName, Catalog.Type.FILESET, "hadoop", "catalog comment", 
properties);
+    Assertions.assertTrue(metalake.catalogExists(catalogName));
+
+    catalog.asSchemas().createSchema(schemaName, "schema comment", properties);
+    Assertions.assertTrue(catalog.asSchemas().schemaExists(schemaName));
+
+    conf.set("fs.gvfs.impl", 
"org.apache.gravitino.filesystem.hadoop.GravitinoVirtualFileSystem");
+    conf.set("fs.AbstractFileSystem.gvfs.impl", 
"org.apache.gravitino.filesystem.hadoop.Gvfs");
+    conf.set("fs.gvfs.impl.disable.cache", "true");
+    conf.set("fs.gravitino.server.uri", serverUri);
+    conf.set("fs.gravitino.client.metalake", metalakeName);
+
+    // Pass this configuration to the real file system
+    conf.set(COSProperties.GRAVITINO_COS_ACCESS_KEY_ID, COS_ACCESS_KEY);
+    conf.set(COSProperties.GRAVITINO_COS_ACCESS_KEY_SECRET, COS_SECRET_KEY);
+    conf.set(COSProperties.GRAVITINO_COS_REGION, COS_REGION);
+    if (StringUtils.isNotBlank(COS_ENDPOINT)) {
+      conf.set(COSProperties.GRAVITINO_COS_ENDPOINT, COS_ENDPOINT);
+    }
+    conf.set("fs.cosn.impl", "org.apache.hadoop.fs.CosFileSystem");
+  }
+
+  @AfterAll
+  public void tearDown() throws IOException {
+    Catalog catalog = metalake.loadCatalog(catalogName);
+    catalog.asSchemas().dropSchema(schemaName, true);
+    metalake.dropCatalog(catalogName, true);
+    client.dropMetalake(metalakeName, true);
+
+    if (client != null) {
+      client.close();
+      client = null;
+    }
+
+    try {
+      closer.close();
+    } catch (Exception e) {
+      LOG.error("Exception in closing CloseableGroup", e);
+    }
+  }
+
+  /**
+   * Remove the `gravitino.bypass` prefix from the configuration and pass it 
to the real file
+   * system. This mirrors {@link
+   * GravitinoVirtualFileSystemOSSIT#convertGvfsConfigToRealFileSystemConfig}.
+   */
+  protected Configuration 
convertGvfsConfigToRealFileSystemConfig(Configuration gvfsConf) {
+    Configuration cosConf = new Configuration();
+    Map<String, String> map = Maps.newHashMap();
+
+    gvfsConf.forEach(entry -> map.put(entry.getKey(), entry.getValue()));
+
+    Map<String, String> hadoopConfMap =
+        FileSystemUtils.toHadoopConfigMap(
+            map, COSFileSystemProvider.GRAVITINO_KEY_TO_COS_HADOOP_KEY);
+
+    hadoopConfMap.forEach(cosConf::set);
+
+    return cosConf;
+  }
+
+  protected String genStorageLocation(String fileset) {
+    return String.format("cosn://%s/%s", BUCKET_NAME, fileset);
+  }
+
+  @Disabled(
+      "COS does not support append, java.io.IOException: The append operation 
is not supported")
+  public void testAppend() throws IOException {}
+
+  protected static boolean cosIsConfigured() {
+    return StringUtils.isNotBlank(System.getenv("COS_ACCESS_KEY_ID"))
+        && StringUtils.isNotBlank(System.getenv("COS_SECRET_ACCESS_KEY"))
+        && StringUtils.isNotBlank(System.getenv("COS_REGION"))
+        && StringUtils.isNotBlank(System.getenv("COS_BUCKET_NAME"));
+  }
+}
diff --git a/docs/fileset-catalog-with-cos.md b/docs/fileset-catalog-with-cos.md
new file mode 100644
index 0000000000..335f44ac26
--- /dev/null
+++ b/docs/fileset-catalog-with-cos.md
@@ -0,0 +1,544 @@
+---
+title: "Fileset Catalog with COS"
+slug: "/fileset-catalog-with-cos"
+date: 2026-6-17
+keyword: "Fileset catalog COS Tencent"
+license: "This software is licensed under the Apache License version 2."
+---
+
+## Introduction
+
+This document explains how to configure a Fileset catalog with Tencent Cloud 
COS (Cloud Object Storage) in Gravitino.
+
+## Prerequisites
+
+To set up a Fileset catalog with COS, follow these steps:
+
+1. Download the 
[`gravitino-tencent-bundle-${gravitino-version}.jar`](https://mvnrepository.com/artifact/org.apache.gravitino/gravitino-tencent-bundle)
 file.
+2. Place the downloaded file into the Gravitino Fileset catalog classpath at 
`${GRAVITINO_HOME}/catalogs/fileset/libs/`.
+3. Start the Gravitino server by running the following command:
+
+```bash
+$ ${GRAVITINO_HOME}/bin/gravitino-server.sh start
+```
+
+Once the server is up and running, you can proceed to configure the Fileset 
catalog with COS. In the rest of this document we will use 
`http://localhost:8090` as the Gravitino server URL, replace with your actual 
server URL.
+
+## COS Catalog Configuration
+
+### COS Fileset Catalog Configuration
+
+In addition to the basic configurations mentioned in [Fileset catalog 
properties](./fileset-catalog.md#catalog-properties), the following properties 
are required to configure a Fileset catalog with COS:
+
+| Configuration item            | Description                                  
                                                                                
                                                                                
                                                                                
                                                                                
                                                  | Default value | Required | 
Since version |
+|-------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|----------|---------------|
+| `cos-region`                  | The region of the Tencent Cloud COS bucket, 
e.g. `ap-guangzhou`, `ap-shanghai`.                                             
                                                                                
                                                                                
                                                                                
                                                   | (none)        | Yes      | 
2.0.0         |
+| `cos-endpoint`                | The endpoint *suffix* of the Tencent Cloud 
COS service (mapped to `fs.cosn.bucket.endpoint_suffix`). It is a host suffix, 
not a full URL — e.g. `cos.ap-guangzhou.myqcloud.com`, **not** 
`https://cos.ap-guangzhou.myqcloud.com`. Optional; when not set, hadoop-cos 
derives it from `cos-region` (`cos.${region}.myqcloud.com`). Set this only if 
you need to point to a non-public endpoint (e.g. an internal/VPC endpoint). | 
(none)        | No       | 2.0.0         |
+| `cos-access-key-id`           | The static access key ID (Tencent Cloud 
`SecretId`) used to access COS data.                                            
                                                                                
                                                                                
                                                                                
                                                       | (none)        | Yes    
  | 2.0.0         |
+| `cos-secret-access-key`       | The static secret access key (Tencent Cloud 
`SecretKey`) used to access COS data.                                           
                                                                                
                                                                                
                                                                                
                                                   | (none)        | Yes      | 
2.0.0         |
+| `credential-providers`        | The credential provider types, separated by 
comma. The currently supported value is `cos-secret-key`. Setting this enables 
credential vending provided by the Gravitino server, so the GVFS client no 
longer needs `cos-access-key-id` / `cos-secret-access-key` locally. See 
[cos-credential-vending](#fileset-with-credential-vending) below for details.   
                                                                 | (none)       
 | No       | 2.0.0         |
+
+:::note
+The fileset catalog automatically loads filesystem providers on the classpath. 
The COS provider
+is registered when the `gravitino-tencent-bundle` jar is present, so 
`default-filesystem-provider`
+and `filesystem-providers` do not need to be set.
+
+`cos-region` is mandatory for hadoop-cos: signing requests, building the 
default endpoint and
+selecting the right CAM scope all require the region. Even if you also set 
`cos-endpoint`, please
+keep `cos-region` set.
+:::
+
+### Schema Configuration
+
+To create a schema, refer to [Schema 
configurations](./fileset-catalog.md#schema-properties).
+
+### Fileset Configuration
+
+For instructions on how to create a fileset, refer to [Fileset 
configurations](./fileset-catalog.md#fileset-properties) for more details.
+
+## Create the Catalog, Schema, and Fileset
+
+This section will show you how to use the Fileset catalog with COS in 
Gravitino, including detailed examples.
+
+### Step 1: Create a Fileset Catalog with COS
+
+First, you need to create a Fileset catalog for COS. The following examples 
demonstrate how to create a Fileset catalog with COS:
+
+<Tabs groupId="language" queryString>
+<TabItem value="shell" label="Shell">
+
+```shell
+curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
+-H "Content-Type: application/json" -d '{
+  "name": "test_catalog",
+  "type": "FILESET",
+  "comment": "This is a COS fileset catalog",
+  "properties": {
+    "location": "cosn://my-bucket-1250000000/root",
+    "cos-region": "ap-guangzhou",
+    "cos-access-key-id": "access_key",
+    "cos-secret-access-key": "secret_key"
+  }
+}' http://localhost:8090/api/metalakes/metalake/catalogs
+```
+
+</TabItem>
+<TabItem value="java" label="Java">
+
+```java
+GravitinoClient gravitinoClient = GravitinoClient
+    .builder("http://localhost:8090";)
+    .withMetalake("metalake")
+    .build();
+
+Map<String, String> cosProperties = ImmutableMap.<String, String>builder()
+    .put("location", "cosn://my-bucket-1250000000/root")
+    .put("cos-region", "ap-guangzhou")
+    .put("cos-access-key-id", "access_key")
+    .put("cos-secret-access-key", "secret_key")
+    .build();
+
+Catalog cosCatalog = gravitinoClient.createCatalog("test_catalog",
+    Type.FILESET,
+    "This is a COS fileset catalog",
+    cosProperties);
+// ...
+
+```
+
+</TabItem>
+<TabItem value="python" label="Python">
+
+```python
+gravitino_client: GravitinoClient = 
GravitinoClient(uri="http://localhost:8090";, metalake_name="metalake")
+cos_properties = {
+    "location": "cosn://my-bucket-1250000000/root",
+    "cos-region": "ap-guangzhou",
+    "cos-access-key-id": "access_key",
+    "cos-secret-access-key": "secret_key"
+}
+
+cos_catalog = gravitino_client.create_catalog(name="test_catalog",
+                                              
catalog_type=Catalog.Type.FILESET,
+                                              provider=None,
+                                              comment="This is a COS fileset 
catalog",
+                                              properties=cos_properties)
+```
+
+</TabItem>
+</Tabs>
+
+:::note
+Tencent Cloud COS bucket names always end with the appid suffix, for example 
`my-bucket-1250000000`. Use that full bucket name in `location` and in any 
`cosn://` URI.
+:::
+
+### Step 2: Create a Schema
+
+Once the Fileset catalog with COS is created, you can create a schema inside 
that catalog. Below are examples of how to do this:
+
+<Tabs groupId="language" queryString>
+<TabItem value="shell" label="Shell">
+
+```shell
+curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
+-H "Content-Type: application/json" -d '{
+  "name": "test_schema",
+  "comment": "This is a COS schema",
+  "properties": {
+    "location": "cosn://my-bucket-1250000000/root/schema"
+  }
+}' http://localhost:8090/api/metalakes/metalake/catalogs/test_catalog/schemas
+```
+
+</TabItem>
+<TabItem value="java" label="Java">
+
+```java
+Catalog catalog = gravitinoClient.loadCatalog("test_catalog");
+
+SupportsSchemas supportsSchemas = catalog.asSchemas();
+
+Map<String, String> schemaProperties = ImmutableMap.<String, String>builder()
+    .put("location", "cosn://my-bucket-1250000000/root/schema")
+    .build();
+Schema schema = supportsSchemas.createSchema("test_schema",
+    "This is a COS schema",
+    schemaProperties
+);
+// ...
+```
+
+</TabItem>
+<TabItem value="python" label="Python">
+
+```python
+gravitino_client: GravitinoClient = 
GravitinoClient(uri="http://localhost:8090";, metalake_name="metalake")
+catalog: Catalog = gravitino_client.load_catalog(name="test_catalog")
+catalog.as_schemas().create_schema(name="test_schema",
+                                   comment="This is a COS schema",
+                                   properties={"location": 
"cosn://my-bucket-1250000000/root/schema"})
+```
+
+</TabItem>
+</Tabs>
+
+### Step 3: Create a Fileset
+
+Now that the schema is created, you can create a fileset inside it. Here's how:
+
+<Tabs groupId="language" queryString>
+<TabItem value="shell" label="Shell">
+
+```shell
+curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
+-H "Content-Type: application/json" -d '{
+  "name": "example_fileset",
+  "comment": "This is an example fileset",
+  "type": "MANAGED",
+  "storageLocation": "cosn://my-bucket-1250000000/root/schema/example_fileset",
+  "properties": {
+    "k1": "v1"
+  }
+}' 
http://localhost:8090/api/metalakes/metalake/catalogs/test_catalog/schemas/test_schema/filesets
+```
+
+</TabItem>
+<TabItem value="java" label="Java">
+
+```java
+GravitinoClient gravitinoClient = GravitinoClient
+    .builder("http://localhost:8090";)
+    .withMetalake("metalake")
+    .build();
+
+Catalog catalog = gravitinoClient.loadCatalog("test_catalog");
+FilesetCatalog filesetCatalog = catalog.asFilesetCatalog();
+
+Map<String, String> propertiesMap = ImmutableMap.<String, String>builder()
+        .put("k1", "v1")
+        .build();
+
+filesetCatalog.createFileset(
+    NameIdentifier.of("test_schema", "example_fileset"),
+    "This is an example fileset",
+    Fileset.Type.MANAGED,
+    "cosn://my-bucket-1250000000/root/schema/example_fileset",
+    propertiesMap);
+```
+
+</TabItem>
+<TabItem value="python" label="Python">
+
+```python
+gravitino_client: GravitinoClient = 
GravitinoClient(uri="http://localhost:8090";, metalake_name="metalake")
+
+catalog: Catalog = gravitino_client.load_catalog(name="test_catalog")
+catalog.as_fileset_catalog().create_fileset(ident=NameIdentifier.of("test_schema",
 "example_fileset"),
+                                            type=Fileset.Type.MANAGED,
+                                            comment="This is an example 
fileset",
+                                            
storage_location="cosn://my-bucket-1250000000/root/schema/example_fileset",
+                                            properties={"k1": "v1"})
+```
+
+</TabItem>
+</Tabs>
+
+## Access a Fileset with COS
+
+### Access the Fileset with the GVFS Java Client
+
+To access fileset with COS using the GVFS Java client, based on the [basic 
GVFS configurations](./how-to-use-gvfs.md#configuration-1), you need to add the 
following configurations:
+
+| Configuration item      | Description                                        
                                                                      | Default 
value | Required | Since version |
+|-------------------------|--------------------------------------------------------------------------------------------------------------------------|---------------|----------|---------------|
+| `cos-region`            | The region of the Tencent Cloud COS bucket.        
                                                                      | (none)  
      | Yes      | 2.0.0         |
+| `cos-endpoint`          | The endpoint *suffix* of the Tencent Cloud COS 
service (e.g. `cos.ap-guangzhou.myqcloud.com`, not a full URL). Optional. | 
(none)        | No       | 2.0.0         |
+| `cos-access-key-id`     | The access key ID (Tencent Cloud SecretId) for COS 
data.                                                                 | (none)  
      | Yes      | 2.0.0         |
+| `cos-secret-access-key` | The secret access key (Tencent Cloud SecretKey) 
for COS data.                                                            | 
(none)        | Yes      | 2.0.0         |
+
+:::note
+If the catalog has enabled [credential 
vending](security/credential-vending.md), the AK/SK properties above can be 
omitted. More details can be found in [Fileset with credential 
vending](#fileset-with-credential-vending).
+:::
+
+```java
+Configuration conf = new Configuration();
+conf.set("fs.AbstractFileSystem.gvfs.impl", 
"org.apache.gravitino.filesystem.hadoop.Gvfs");
+conf.set("fs.gvfs.impl", 
"org.apache.gravitino.filesystem.hadoop.GravitinoVirtualFileSystem");
+conf.set("fs.gravitino.server.uri", "http://localhost:8090";);
+conf.set("fs.gravitino.client.metalake", "test_metalake");
+conf.set("cos-region", "ap-guangzhou");
+conf.set("cos-access-key-id", "access_key");
+conf.set("cos-secret-access-key", "secret_key");
+Path filesetPath = new 
Path("gvfs://fileset/test_catalog/test_schema/test_fileset/new_dir");
+FileSystem fs = filesetPath.getFileSystem(conf);
+fs.mkdirs(filesetPath);
+...
+```
+
+Similar to Spark configurations, you need to add COS (bundle) jars to the 
classpath according to your environment.
+If you want to customise your hadoop version or there is already a hadoop 
version in your project, you can add the following dependencies to your 
`pom.xml`:
+
+```xml
+  <dependency>
+    <groupId>org.apache.hadoop</groupId>
+    <artifactId>hadoop-common</artifactId>
+    <version>${HADOOP_VERSION}</version>
+  </dependency>
+
+  <!-- hadoop-cos is published by Tencent Cloud, not by Apache Hadoop. -->
+  <dependency>
+    <groupId>com.qcloud.cos</groupId>
+    <artifactId>hadoop-cos</artifactId>
+    <version>3.3.0-8.3.23</version>
+  </dependency>
+
+  <dependency>
+    <groupId>org.apache.gravitino</groupId>
+    <artifactId>gravitino-filesystem-hadoop3-runtime</artifactId>
+    <version>${GRAVITINO_VERSION}</version>
+  </dependency>
+```
+
+:::note
+Unlike the S3, OSS, GCS and Azure connectors, COS does **not** ship with 
Apache Hadoop. The HCFS adapter for COS is published by Tencent Cloud as 
`com.qcloud.cos:hadoop-cos`. Make sure the version you pick is compatible with 
your Hadoop version (the `<hadoop>-<sdk>` form encodes both, e.g. 
`3.3.0-8.3.23` targets Hadoop 3.3.0).
+:::
+
+Or use the bundle jar with Hadoop environment if there is no Hadoop 
environment:
+
+```xml
+  <dependency>
+    <groupId>org.apache.gravitino</groupId>
+    <artifactId>gravitino-tencent-bundle</artifactId>
+    <version>${GRAVITINO_VERSION}</version>
+  </dependency>
+
+  <dependency>
+    <groupId>org.apache.gravitino</groupId>
+    <artifactId>gravitino-filesystem-hadoop3-runtime</artifactId>
+    <version>${GRAVITINO_VERSION}</version>
+  </dependency>
+```
+
+### Access the Fileset with Spark
+
+The following code snippet shows how to use **PySpark 3.5.0 with Hadoop 
environment(Hadoop 3.3.4)** to access the fileset:
+
+Before running the following code, you need to install required packages:
+
+```bash
+pip install pyspark==3.5.0
+pip install apache-gravitino==${GRAVITINO_VERSION}
+```
+Then you can run the following code:
+
+```python
+from pyspark.sql import SparkSession
+import os
+
+gravitino_url = "http://localhost:8090";
+metalake_name = "test"
+
+catalog_name = "your_cos_catalog"
+schema_name = "your_cos_schema"
+fileset_name = "your_cos_fileset"
+
+# JDK8 as follows. JDK17 will be slightly different, you need to add
+# '--conf 
"spark.driver.extraJavaOptions=--add-opens=java.base/sun.nio.ch=ALL-UNNAMED"
+#  --conf 
"spark.executor.extraJavaOptions=--add-opens=java.base/sun.nio.ch=ALL-UNNAMED"'
+# to the submit args.
+os.environ["PYSPARK_SUBMIT_ARGS"] = (
+    "--jars "
+    "/path/to/gravitino-filesystem-hadoop3-runtime-{gravitino-version}.jar,"
+    "/path/to/hadoop-cos-3.3.0-8.3.23.jar,"
+    "/path/to/cos_api-bundle-5.6.227.jar "
+    "--master local[1] pyspark-shell"
+)
+spark = SparkSession.builder \
+    .appName("cos_fileset_test") \
+    .config("spark.hadoop.fs.AbstractFileSystem.gvfs.impl", 
"org.apache.gravitino.filesystem.hadoop.Gvfs") \
+    .config("spark.hadoop.fs.gvfs.impl", 
"org.apache.gravitino.filesystem.hadoop.GravitinoVirtualFileSystem") \
+    .config("spark.hadoop.fs.gravitino.server.uri", gravitino_url) \
+    .config("spark.hadoop.fs.gravitino.client.metalake", "test") \
+    .config("spark.hadoop.cos-region", "ap-guangzhou") \
+    .config("spark.hadoop.cos-access-key-id", os.environ["COS_ACCESS_KEY_ID"]) 
\
+    .config("spark.hadoop.cos-secret-access-key", 
os.environ["COS_SECRET_ACCESS_KEY"]) \
+    .config("spark.driver.memory", "2g") \
+    .config("spark.driver.port", "2048") \
+    .getOrCreate()
+
+data = [("Alice", 25), ("Bob", 30), ("Cathy", 45)]
+columns = ["Name", "Age"]
+spark_df = spark.createDataFrame(data, schema=columns)
+gvfs_path = 
f"gvfs://fileset/{catalog_name}/{schema_name}/{fileset_name}/people"
+
+spark_df.coalesce(1).write \
+    .mode("overwrite") \
+    .option("header", "true") \
+    .csv(gvfs_path)
+```
+
+If your Spark is **without Hadoop environment**, you can use the following 
code snippet to access the fileset:
+
+```python
+## Only the PYSPARK_SUBMIT_ARGS line below changes; keep all the 
SparkSession.builder.config(...)
+## calls (including spark.hadoop.cos-region / cos-access-key-id / 
cos-secret-access-key) above as-is.
+
+os.environ["PYSPARK_SUBMIT_ARGS"] = "--jars 
/path/to/gravitino-tencent-bundle-{gravitino-version}.jar,/path/to/gravitino-filesystem-hadoop3-runtime-{gravitino-version}.jar
 --master local[1] pyspark-shell"
+```
+
+- 
[`gravitino-tencent-bundle-${gravitino-version}.jar`](https://mvnrepository.com/artifact/org.apache.gravitino/gravitino-tencent-bundle):
 A "fat" JAR that includes `gravitino-tencent` functionality and all necessary 
dependencies like `hadoop-cos` and the Tencent Cloud COS Java SDK. Use this if 
your Spark environment doesn't have a pre-existing Hadoop setup.
+- 
[`gravitino-filesystem-hadoop3-runtime-${gravitino-version}.jar`](https://mvnrepository.com/artifact/org.apache.gravitino/gravitino-filesystem-hadoop3-runtime):
 A "fat" JAR that bundles Gravitino's virtual filesystem client and includes 
the functionality of `gravitino-tencent`. It is required for accessing 
Gravitino filesets.
+
+Please choose the correct jar according to your environment.
+
+:::note
+In some Spark versions, a Hadoop environment is needed by the driver, adding 
the bundle jars with `--jars` may not work. If this is the case, you should add 
the jars to the spark CLASSPATH directly.
+:::
+
+### Access a Fileset Using the Hadoop Fs Command
+
+The following are examples of how to use the `hadoop fs` command to access the 
fileset in Hadoop 3.1.3:
+
+1. Add the following contents to the `${HADOOP_HOME}/etc/hadoop/core-site.xml` 
file:
+
+```xml
+  <property>
+    <name>fs.AbstractFileSystem.gvfs.impl</name>
+    <value>org.apache.gravitino.filesystem.hadoop.Gvfs</value>
+  </property>
+
+  <property>
+    <name>fs.gvfs.impl</name>
+    
<value>org.apache.gravitino.filesystem.hadoop.GravitinoVirtualFileSystem</value>
+  </property>
+
+  <property>
+    <name>fs.gravitino.server.uri</name>
+    <value>http://localhost:8090</value>
+  </property>
+
+  <property>
+    <name>fs.gravitino.client.metalake</name>
+    <value>test</value>
+  </property>
+
+  <property>
+    <name>cos-region</name>
+    <value>ap-guangzhou</value>
+  </property>
+
+  <property>
+    <name>cos-access-key-id</name>
+    <value>access-key</value>
+  </property>
+
+  <property>
+    <name>cos-secret-access-key</name>
+    <value>secret-key</value>
+  </property>
+```
+
+2. Add the necessary jars to the Hadoop classpath.
+
+For COS, you need to add 
`gravitino-filesystem-hadoop3-runtime-${gravitino-version}.jar`, 
`hadoop-cos-3.3.0-8.3.23.jar` and `cos_api-bundle-5.6.227.jar` to the Hadoop 
classpath. Unlike `hadoop-aws` or `hadoop-aliyun`, these jars are *not* part of 
the Apache Hadoop distribution; download them from Maven Central and place them 
under `${HADOOP_HOME}/share/hadoop/tools/lib/`.
+
+3. Run the following command to access the fileset:
+
+```shell
+./${HADOOP_HOME}/bin/hadoop fs -ls 
gvfs://fileset/cos_catalog/cos_schema/cos_fileset
+./${HADOOP_HOME}/bin/hadoop fs -put /path/to/local/file 
gvfs://fileset/cos_catalog/cos_schema/cos_fileset
+```
+
+### Access the Fileset with the GVFS Python Client / Pandas
+
+:::note
+The GVFS **Python** client does not yet ship a COS storage handler, so reading
+and writing COS-backed filesets via the Python 
`gvfs.GravitinoVirtualFileSystem`
+or pandas `read_csv("gvfs://...")` is **not supported in this release**. Only
+the GVFS **Java** client (and Spark / `hadoop fs` on top of it) can do fileset
+I/O against COS today.
+
+The Python `GravitinoClient` itself is not affected: you can still create,
+inspect, update and delete COS catalogs, schemas and filesets through the
+metadata API exactly as shown in the [Step 1 example 
above](#step-1-create-a-fileset-catalog-with-cos) and the following Step 2 / 
Step 3 sections.
+What is missing is purely the *data plane* (`gvfs.ls`, `gvfs.open`,
+pandas `read_csv("gvfs://...")`, etc.) for COS, which will be added in a
+follow-up release together with a `COSStorageHandler` and the corresponding
+fsspec adapter.
+:::
+
+For other use cases of the Java GVFS client, refer to the [Gravitino Virtual 
File System](./how-to-use-gvfs.md) document.
+
+## Fileset with Credential Vending
+
+Since 2.0.0, Gravitino supports credential vending for the COS fileset. If the 
catalog has been [configured with 
credential](./security/credential-vending.md), you can access COS fileset 
without providing authentication information like `cos-access-key-id` and 
`cos-secret-access-key` in the GVFS client configuration.
+
+The currently supported credential providers are listed below:
+
+| Credential provider | Description                                            
                                                                                
                                    | Vended credential type |
+|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------|
+| `cos-secret-key`    | The Gravitino server hands out the static 
`cos-access-key-id` / `cos-secret-access-key` configured on the catalog. Useful 
for centralising credentials on the server side. | Static AK/SK           |
+
+:::note
+STS-token-based credential vending (`cos-token`) for COS, including role-arn / 
assume-role configuration, will be added in a follow-up release. Until then, 
only the static `cos-secret-key` provider is available.
+:::
+
+### Create a COS Fileset Catalog with Credential Vending
+
+In addition to the configuration described in [COS fileset catalog 
configuration](#cos-fileset-catalog-configuration), you only need to set 
`credential-providers` to `cos-secret-key` to enable credential vending:
+
+```shell
+curl -X POST -H "Accept: application/vnd.gravitino.v1+json" \
+-H "Content-Type: application/json" -d '{
+  "name": "cos-catalog-with-credential-vending",
+  "type": "FILESET",
+  "comment": "This is a COS fileset catalog with credential vending",
+  "properties": {
+    "location": "cosn://my-bucket-1250000000/root",
+    "cos-region": "ap-guangzhou",
+    "cos-access-key-id": "access_key",
+    "cos-secret-access-key": "secret_key",
+    "credential-providers": "cos-secret-key"
+  }
+}' http://localhost:8090/api/metalakes/metalake/catalogs
+```
+
+### Access a COS Fileset with Credential Vending
+
+When the catalog is configured with credentials and client-side credential 
vending is enabled,
+you can access COS filesets directly using the GVFS Java client or Spark 
without providing AK/SK in the client.
+
+GVFS Java client:
+
+```java
+Configuration conf = new Configuration();
+conf.setBoolean("fs.gravitino.enableCredentialVending", true);
+conf.set("fs.AbstractFileSystem.gvfs.impl", 
"org.apache.gravitino.filesystem.hadoop.Gvfs");
+conf.set("fs.gvfs.impl", 
"org.apache.gravitino.filesystem.hadoop.GravitinoVirtualFileSystem");
+conf.set("fs.gravitino.server.uri", "http://localhost:8090";);
+conf.set("fs.gravitino.client.metalake", "test_metalake");
+// No need to set cos-access-key-id and cos-secret-access-key
+Path filesetPath = new 
Path("gvfs://fileset/cos_test_catalog/test_schema/test_fileset/new_dir");
+FileSystem fs = filesetPath.getFileSystem(conf);
+fs.mkdirs(filesetPath);
+...
+```
+
+Spark:
+
+```python
+spark = SparkSession.builder \
+    .appName("cos_fileset_test") \
+    .config("spark.hadoop.fs.gravitino.enableCredentialVending", "true") \
+    .config("spark.hadoop.fs.AbstractFileSystem.gvfs.impl", 
"org.apache.gravitino.filesystem.hadoop.Gvfs") \
+    .config("spark.hadoop.fs.gvfs.impl", 
"org.apache.gravitino.filesystem.hadoop.GravitinoVirtualFileSystem") \
+    .config("spark.hadoop.fs.gravitino.server.uri", "http://localhost:8090";) \
+    .config("spark.hadoop.fs.gravitino.client.metalake", "test") \
+    .config("spark.driver.memory", "2g") \
+    .config("spark.driver.port", "2048") \
+    .getOrCreate()
+# Note: no need to set spark.hadoop.cos-access-key-id / cos-secret-access-key 
here —
+# credential vending will fetch them from the Gravitino server.
+```
+
+The Hadoop `fs` command is similar to the above examples. The GVFS **Python** 
client cannot access COS-backed filesets yet — see the note in [Access the 
Fileset with the GVFS Python Client / 
Pandas](#access-the-fileset-with-the-gvfs-python-client--pandas).
diff --git a/docs/fileset-catalog.md b/docs/fileset-catalog.md
index bb7b315fd1..599e1f8eaf 100644
--- a/docs/fileset-catalog.md
+++ b/docs/fileset-catalog.md
@@ -12,9 +12,10 @@ Fileset catalog is a fileset catalog that using Hadoop 
Compatible File System (H
 the storage location of the fileset. It supports the local filesystem and 
HDFS. Since
 0.7.0-incubating, Gravitino supports [S3](fileset-catalog-with-s3.md), 
[GCS](fileset-catalog-with-gcs.md),
 [OSS](fileset-catalog-with-oss.md) and [Azure Blob 
Storage](fileset-catalog-with-adls.md) through Fileset catalog.
+Since 2.0.0, Gravitino also supports [Tencent Cloud 
COS](fileset-catalog-with-cos.md).
 
 The rest of this document will use HDFS or local file as an example to 
illustrate how to use the Fileset catalog.
-For S3, GCS, OSS and Azure Blob Storage, the configuration is similar to HDFS,
+For S3, GCS, OSS, Azure Blob Storage and COS, the configuration is similar to 
HDFS,
 refer to the corresponding document for more details.
 
 Note that Gravitino uses Hadoop 3 dependencies to build Fileset catalog. 
Theoretically, it should be
@@ -43,7 +44,7 @@ the Fileset catalog has the following properties:
 | `fs.path.config.<name>`              | Defines a logical location entry. Set 
`fs.path.config.<name>` to the real base URI (for example, `hdfs://cluster1/`). 
Any key that starts with the same prefix (such as 
`fs.path.config.<name>.config.resource`) is treated as a location-scoped 
property and will be forwarded to the underlying filesystem client.            
| (none)          | No       | 1.2.0            |
 
 :::note
-`default-filesystem-provider` and `filesystem-providers` are deprecated since 
1.2.0. The fileset catalog automatically loads filesystem providers on the 
classpath, including buildin filesystem provider and cloud providers when the 
corresponding bundle jar is present (for example, `gravitino-aws-bundle`, 
`gravitino-azure-bundle`, `gravitino-aliyun-bundle`, or `gravitino-gcp-bundle`).
+`default-filesystem-provider` and `filesystem-providers` are deprecated since 
1.2.0. The fileset catalog automatically loads filesystem providers on the 
classpath, including the built-in filesystem provider and cloud providers when 
the corresponding bundle jar is present (for example, `gravitino-aws-bundle`, 
`gravitino-azure-bundle`, `gravitino-aliyun-bundle`, `gravitino-gcp-bundle`, or 
`gravitino-tencent-bundle`).
 :::
 
 Refer to [Credential vending](./security/credential-vending.md) for more 
details about credential vending.
@@ -75,13 +76,14 @@ The Gravitino Fileset extends the following properties in 
the `xxx-site.xml`:
 ### Fileset Catalog with Cloud Storage
 
 In the current implementation, the fileset uses the HDFS protocol to access 
its location. If users use S3, GCS, OSS,
-or Azure Blob Storage, they can also configure the `config.resources` to 
specify custom configuration
+Azure Blob Storage or Tencent Cloud COS, they can also configure the 
`config.resources` to specify custom configuration
 files.
 
 - For S3, refer to [Fileset-catalog-with-s3](./fileset-catalog-with-s3.md) for 
more details.
 - For GCS, refer to [Fileset-catalog-with-gcs](./fileset-catalog-with-gcs.md) 
for more details.
 - For OSS, refer to [Fileset-catalog-with-oss](./fileset-catalog-with-oss.md) 
for more details.
 - For Azure Blob Storage, refer to 
[Fileset-catalog-with-adls](./fileset-catalog-with-adls.md) for more details.
+- For Tencent Cloud COS, refer to 
[Fileset-catalog-with-cos](./fileset-catalog-with-cos.md) for more details.
 
 ### Implement a Custom HCFS File System Fileset
 
@@ -96,11 +98,11 @@ the jar 
[gravitino-hadoop-common](https://repo1.maven.org/maven2/org/apache/grav
       throws IOException;
   
   // The schema name of the file system provider. 'file' for Local file system,
-  // 'hdfs' for HDFS, 's3a' for AWS S3, 'gs' for GCS, 'oss' for Aliyun OSS. 
+  // 'hdfs' for HDFS, 's3a' for AWS S3, 'gs' for GCS, 'oss' for Aliyun OSS, 
'cosn' for Tencent Cloud COS.
   String scheme();
 
   // Name of the file system provider. 'builtin-local' for Local file system, 
'builtin-hdfs' for HDFS, 
-  // 's3' for AWS S3, 'gcs' for GCS, 'oss' for Aliyun OSS.
+  // 's3' for AWS S3, 'gcs' for GCS, 'oss' for Aliyun OSS, 'cos' for Tencent 
Cloud COS.
   String name();
 ```
 
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6c93c5af40..9222b53ab3 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -45,6 +45,7 @@ hadoop3 = "3.3.6"
 hadoop3-gcs = "1.9.4-hadoop3"
 hadoop3-abs = "3.3.6"
 hadoop3-aliyun = "3.3.6"
+hadoop-cos = "3.3.0-8.3.23"
 hadoop3-thirdparty = "1.1.1"
 hadoop-minikdc = "3.3.6"
 re2j = "1.7"
@@ -239,6 +240,7 @@ hadoop3-mapreduce-client-core = { group = 
"org.apache.hadoop", name = "hadoop-ma
 hadoop3-minicluster = { group = "org.apache.hadoop", name = 
"hadoop-minicluster", version.ref = "hadoop-minikdc"}
 hadoop3-gcs = { group = "com.google.cloud.bigdataoss", name = "gcs-connector", 
version.ref = "hadoop3-gcs"}
 hadoop3-oss = { group = "org.apache.hadoop", name = "hadoop-aliyun", 
version.ref = "hadoop3-aliyun"}
+hadoop3-cos = { group = "com.qcloud.cos", name = "hadoop-cos", version.ref = 
"hadoop-cos"}
 hadoop3-abs = { group = "org.apache.hadoop", name = "hadoop-azure", 
version.ref = "hadoop3-abs"}
 hadoop3-shaded-guava = { group = "org.apache.hadoop.thirdparty", name = 
"hadoop-shaded-guava", version.ref = "hadoop3-thirdparty" }
 hadoop3-shaded-protobuf = { group = "org.apache.hadoop.thirdparty", name = 
"hadoop-shaded-protobuf_3_7", version.ref = "hadoop3-thirdparty" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index c8a705ab7a..3f9ec2f06a 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -123,6 +123,7 @@ include("integration-test-common")
 include(":bundles:aws", ":bundles:aws-bundle", ":bundles:iceberg-aws-bundle")
 include(":bundles:gcp", ":bundles:gcp-bundle", ":bundles:iceberg-gcp-bundle")
 include(":bundles:aliyun", ":bundles:aliyun-bundle", 
":bundles:iceberg-aliyun-bundle")
+include(":bundles:tencent", ":bundles:tencent-bundle")
 include(":bundles:azure", ":bundles:azure-bundle", 
":bundles:iceberg-azure-bundle")
 include(":catalogs:hadoop-common")
 include(":catalogs:hadoop-auth")

Reply via email to