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

fanng 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 cf09f9ab0 [#5068]  feat(core): support GCS token provider (#5224)
cf09f9ab0 is described below

commit cf09f9ab0a93bb75626daa8c098c47cdfb545e1e
Author: FANNG <[email protected]>
AuthorDate: Fri Oct 25 14:16:51 2024 +0800

    [#5068]  feat(core): support GCS token provider (#5224)
    
    ### What changes were proposed in this pull request?
    support GCS token provider
    
    ### Why are the changes needed?
    
    Fix: #5068
    
    ### Does this PR introduce _any_ user-facing change?
    no
    
    ### How was this patch tested?
    
    add IT and run with real google acount
---
 LICENSE.bin                                        |   3 +
 .../gravitino/credential/GCSTokenCredential.java   |  73 +++++++
 bundles/gcp-bundle/build.gradle.kts                |  11 +-
 .../gravitino/gcs/credential/GCSTokenProvider.java | 218 +++++++++++++++++++++
 ....apache.gravitino.credential.CredentialProvider |  19 ++
 .../services/org.apache.hadoop.fs.FileSystem       |  20 ++
 .../gravitino/credential/CredentialConstants.java  |   2 +
 .../credential/CredentialPropertyUtils.java        |  25 ++-
 .../credential/config/GCSCredentialConfig.java     |  51 +++++
 gradle/libs.versions.toml                          |   5 +
 iceberg/iceberg-rest-server/build.gradle.kts       |   2 +
 .../iceberg/integration/test/IcebergRESTGCSIT.java | 108 ++++++++++
 .../integration/test/IcebergRESTJdbcCatalogIT.java |  24 ++-
 .../integration/test/IcebergRESTServiceBaseIT.java |  56 +++++-
 .../gravitino/integration/test/util/BaseIT.java    |  37 ++--
 .../gravitino/integration/test/util/ITUtils.java   |  16 ++
 settings.gradle.kts                                |   3 +-
 17 files changed, 634 insertions(+), 39 deletions(-)

diff --git a/LICENSE.bin b/LICENSE.bin
index e922f9367..1bdb9864d 100644
--- a/LICENSE.bin
+++ b/LICENSE.bin
@@ -306,6 +306,7 @@
    Apache Iceberg core
    Apache Iceberg Hive metastore
    Apache Iceberg GCP
+   Apache Iceberg GCP bundle
    Apache Ivy
    Apache Log4j 1.x Compatibility API
    Apache Log4j API
@@ -398,6 +399,8 @@
    RE2/J
    ZSTD JNI
    fsspec
+   Google auth HTTP
+   Google auth Credentials
 
    This product bundles various third-party components also under the
    MIT license
diff --git 
a/api/src/main/java/org/apache/gravitino/credential/GCSTokenCredential.java 
b/api/src/main/java/org/apache/gravitino/credential/GCSTokenCredential.java
new file mode 100644
index 000000000..98186e2de
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/credential/GCSTokenCredential.java
@@ -0,0 +1,73 @@
+/*
+ *  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;
+
+/** The GCS token credential to access GCS. */
+public class GCSTokenCredential implements Credential {
+
+  /** GCS credential type. */
+  public static final String GCS_TOKEN_CREDENTIAL_TYPE = "gcs-token";
+
+  /** GCS credential property, token name. */
+  public static final String GCS_TOKEN_NAME = "token";
+
+  private String token;
+  private long expireMs;
+
+  /**
+   * @param token The GCS token.
+   * @param expireMs The GCS token expire time at ms.
+   */
+  public GCSTokenCredential(String token, long expireMs) {
+    Preconditions.checkArgument(
+        StringUtils.isNotBlank(token), "GCS session token should not be null");
+    this.token = token;
+    this.expireMs = expireMs;
+  }
+
+  @Override
+  public String credentialType() {
+    return GCS_TOKEN_CREDENTIAL_TYPE;
+  }
+
+  @Override
+  public long expireTimeInMs() {
+    return expireMs;
+  }
+
+  @Override
+  public Map<String, String> credentialInfo() {
+    return (new ImmutableMap.Builder<String, String>()).put(GCS_TOKEN_NAME, 
token).build();
+  }
+
+  /**
+   * Get GCS token.
+   *
+   * @return The GCS token.
+   */
+  public String token() {
+    return token;
+  }
+}
diff --git a/bundles/gcp-bundle/build.gradle.kts 
b/bundles/gcp-bundle/build.gradle.kts
index 6b373578c..e69ff345e 100644
--- a/bundles/gcp-bundle/build.gradle.kts
+++ b/bundles/gcp-bundle/build.gradle.kts
@@ -25,9 +25,17 @@ plugins {
 }
 
 dependencies {
+  compileOnly(project(":api"))
+  compileOnly(project(":core"))
+  compileOnly(project(":catalogs:catalog-common"))
   compileOnly(project(":catalogs:catalog-hadoop"))
+
   compileOnly(libs.hadoop3.common)
+
+  implementation(libs.commons.lang3)
   implementation(libs.hadoop3.gcs)
+  implementation(libs.google.auth.http)
+  implementation(libs.google.auth.credentials)
 }
 
 tasks.withType(ShadowJar::class.java) {
@@ -38,8 +46,7 @@ tasks.withType(ShadowJar::class.java) {
   // Relocate dependencies to avoid conflicts
   relocate("org.apache.httpcomponents", 
"org.apache.gravitino.shaded.org.apache.httpcomponents")
   relocate("org.apache.commons", 
"org.apache.gravitino.shaded.org.apache.commons")
-  relocate("com.google.guava", "org.apache.gravitino.shaded.com.google.guava")
-  relocate("com.google.code", "org.apache.gravitino.shaded.com.google.code")
+  relocate("com.google", "org.apache.gravitino.shaded.com.google")
 }
 
 tasks.jar {
diff --git 
a/bundles/gcp-bundle/src/main/java/org/apache/gravitino/gcs/credential/GCSTokenProvider.java
 
b/bundles/gcp-bundle/src/main/java/org/apache/gravitino/gcs/credential/GCSTokenProvider.java
new file mode 100644
index 000000000..94234b2d9
--- /dev/null
+++ 
b/bundles/gcp-bundle/src/main/java/org/apache/gravitino/gcs/credential/GCSTokenProvider.java
@@ -0,0 +1,218 @@
+/*
+ *  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.gcs.credential;
+
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.CredentialAccessBoundary;
+import com.google.auth.oauth2.CredentialAccessBoundary.AccessBoundaryRule;
+import com.google.auth.oauth2.DownscopedCredentials;
+import com.google.auth.oauth2.GoogleCredentials;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.credential.Credential;
+import org.apache.gravitino.credential.CredentialConstants;
+import org.apache.gravitino.credential.CredentialContext;
+import org.apache.gravitino.credential.CredentialProvider;
+import org.apache.gravitino.credential.GCSTokenCredential;
+import org.apache.gravitino.credential.PathBasedCredentialContext;
+import org.apache.gravitino.credential.config.GCSCredentialConfig;
+
+/** Generate GCS access token according to the read and write paths. */
+public class GCSTokenProvider implements CredentialProvider {
+
+  private static final String INITIAL_SCOPE = 
"https://www.googleapis.com/auth/cloud-platform";;
+
+  private GoogleCredentials sourceCredentials;
+
+  @Override
+  public void initialize(Map<String, String> properties) {
+    GCSCredentialConfig gcsCredentialConfig = new 
GCSCredentialConfig(properties);
+    try {
+      this.sourceCredentials =
+          
getSourceCredentials(gcsCredentialConfig).createScoped(INITIAL_SCOPE);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  public void close() {}
+
+  @Override
+  public String credentialType() {
+    return CredentialConstants.GCS_TOKEN_CREDENTIAL_PROVIDER_TYPE;
+  }
+
+  @Override
+  public Credential getCredential(CredentialContext context) {
+    if (!(context instanceof PathBasedCredentialContext)) {
+      return null;
+    }
+    PathBasedCredentialContext pathBasedCredentialContext = 
(PathBasedCredentialContext) context;
+    try {
+      AccessToken accessToken =
+          getToken(
+              pathBasedCredentialContext.getReadPaths(),
+              pathBasedCredentialContext.getWritePaths());
+      String tokenValue = accessToken.getTokenValue();
+      long expireTime = 
accessToken.getExpirationTime().toInstant().toEpochMilli();
+      return new GCSTokenCredential(tokenValue, expireTime);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private AccessToken getToken(Set<String> readLocations, Set<String> 
writeLocations)
+      throws IOException {
+    DownscopedCredentials downscopedCredentials =
+        DownscopedCredentials.newBuilder()
+            .setSourceCredential(sourceCredentials)
+            .setCredentialAccessBoundary(getAccessBoundary(readLocations, 
writeLocations))
+            .build();
+    return downscopedCredentials.refreshAccessToken();
+  }
+
+  private CredentialAccessBoundary getAccessBoundary(
+      Set<String> readLocations, Set<String> writeLocations) {
+    // bucketName -> read resource expressions
+    Map<String, List<String>> readExpressions = new HashMap<>();
+    // bucketName -> write resource expressions
+    Map<String, List<String>> writeExpressions = new HashMap<>();
+
+    // Construct read and write resource expressions
+    HashSet<String> readBuckets = new HashSet<>();
+    HashSet<String> writeBuckets = new HashSet<>();
+    Stream.concat(readLocations.stream(), writeLocations.stream())
+        .distinct()
+        .forEach(
+            location -> {
+              URI uri = URI.create(location);
+              String bucketName = getBucketName(uri);
+              readBuckets.add(bucketName);
+              String resourcePath = uri.getPath().substring(1);
+              List<String> resourceExpressions =
+                  readExpressions.computeIfAbsent(bucketName, key -> new 
ArrayList<>());
+              // add read privilege
+              resourceExpressions.add(
+                  String.format(
+                      
"resource.name.startsWith('projects/_/buckets/%s/objects/%s')",
+                      bucketName, resourcePath));
+              // add list privilege
+              resourceExpressions.add(
+                  String.format(
+                      
"api.getAttribute('storage.googleapis.com/objectListPrefix', 
'').startsWith('%s')",
+                      resourcePath));
+              if (writeLocations.contains(location)) {
+                writeBuckets.add(bucketName);
+                resourceExpressions =
+                    writeExpressions.computeIfAbsent(bucketName, key -> new 
ArrayList<>());
+                // add write privilege
+                resourceExpressions.add(
+                    String.format(
+                        
"resource.name.startsWith('projects/_/buckets/%s/objects/%s')",
+                        bucketName, resourcePath));
+              }
+            });
+
+    // Construct policy according to the resource expression and privilege.
+    CredentialAccessBoundary.Builder credentialAccessBoundaryBuilder =
+        CredentialAccessBoundary.newBuilder();
+    readBuckets.forEach(
+        bucket -> {
+          List<String> readConditions = readExpressions.get(bucket);
+          AccessBoundaryRule rule =
+              getAccessBoundaryRule(
+                  bucket,
+                  readConditions,
+                  Arrays.asList(
+                      "inRole:roles/storage.legacyObjectReader",
+                      "inRole:roles/storage.objectViewer"));
+          if (rule == null) {
+            return;
+          }
+          credentialAccessBoundaryBuilder.addRule(rule);
+        });
+
+    writeBuckets.forEach(
+        bucket -> {
+          List<String> writeConditions = writeExpressions.get(bucket);
+          AccessBoundaryRule rule =
+              getAccessBoundaryRule(
+                  bucket,
+                  writeConditions,
+                  Arrays.asList("inRole:roles/storage.legacyBucketWriter"));
+          if (rule == null) {
+            return;
+          }
+          credentialAccessBoundaryBuilder.addRule(rule);
+        });
+
+    return credentialAccessBoundaryBuilder.build();
+  }
+
+  private AccessBoundaryRule getAccessBoundaryRule(
+      String bucketName, List<String> resourceExpression, List<String> 
permissions) {
+    if (resourceExpression == null || resourceExpression.isEmpty()) {
+      return null;
+    }
+    CredentialAccessBoundary.AccessBoundaryRule.Builder builder =
+        CredentialAccessBoundary.AccessBoundaryRule.newBuilder();
+    builder.setAvailableResource(toGCSBucketResource(bucketName));
+    builder.setAvailabilityCondition(
+        
CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition.newBuilder()
+            .setExpression(String.join(" || ", resourceExpression))
+            .build());
+    builder.setAvailablePermissions(permissions);
+    return builder.build();
+  }
+
+  private static String toGCSBucketResource(String bucketName) {
+    return "//storage.googleapis.com/projects/_/buckets/" + bucketName;
+  }
+
+  private static String getBucketName(URI uri) {
+    return uri.getHost();
+  }
+
+  private GoogleCredentials getSourceCredentials(GCSCredentialConfig 
gcsCredentialConfig)
+      throws IOException {
+    String gcsCredentialFilePath = gcsCredentialConfig.gcsCredentialFilePath();
+    if (StringUtils.isBlank(gcsCredentialFilePath)) {
+      return GoogleCredentials.getApplicationDefault();
+    } else {
+      File credentialsFile = new File(gcsCredentialFilePath);
+      if (!credentialsFile.exists()) {
+        throw new IOException("GCS credential file does not exist." + 
gcsCredentialFilePath);
+      }
+      return GoogleCredentials.fromStream(new 
FileInputStream(credentialsFile));
+    }
+  }
+}
diff --git 
a/bundles/gcp-bundle/src/main/resources/META-INF/services/org.apache.gravitino.credential.CredentialProvider
 
b/bundles/gcp-bundle/src/main/resources/META-INF/services/org.apache.gravitino.credential.CredentialProvider
new file mode 100644
index 000000000..695104905
--- /dev/null
+++ 
b/bundles/gcp-bundle/src/main/resources/META-INF/services/org.apache.gravitino.credential.CredentialProvider
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+org.apache.gravitino.gcs.credential.GCSTokenProvider
diff --git 
a/bundles/gcp-bundle/src/main/resources/META-INF/services/org.apache.hadoop.fs.FileSystem
 
b/bundles/gcp-bundle/src/main/resources/META-INF/services/org.apache.hadoop.fs.FileSystem
new file mode 100644
index 000000000..e67410de7
--- /dev/null
+++ 
b/bundles/gcp-bundle/src/main/resources/META-INF/services/org.apache.hadoop.fs.FileSystem
@@ -0,0 +1,20 @@
+#
+# 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.
+#
+
+org.apache.gravitino.shaded.com.google.cloud.hadoop.fs.gcs.GoogleHadoopFileSystem
diff --git 
a/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/CredentialConstants.java
 
b/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/CredentialConstants.java
index 596268395..a141b637e 100644
--- 
a/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/CredentialConstants.java
+++ 
b/catalogs/catalog-common/src/main/java/org/apache/gravitino/credential/CredentialConstants.java
@@ -22,5 +22,7 @@ package org.apache.gravitino.credential;
 public class CredentialConstants {
   public static final String CREDENTIAL_PROVIDER_TYPE = 
"credential-provider-type";
 
+  public static final String GCS_TOKEN_CREDENTIAL_PROVIDER_TYPE = "gcs-token";
+
   private CredentialConstants() {}
 }
diff --git 
a/common/src/main/java/org/apache/gravitino/credential/CredentialPropertyUtils.java
 
b/common/src/main/java/org/apache/gravitino/credential/CredentialPropertyUtils.java
index 255e54fbf..e380cc5d4 100644
--- 
a/common/src/main/java/org/apache/gravitino/credential/CredentialPropertyUtils.java
+++ 
b/common/src/main/java/org/apache/gravitino/credential/CredentialPropertyUtils.java
@@ -19,12 +19,17 @@
 
 package org.apache.gravitino.credential;
 
+import com.google.common.collect.ImmutableMap;
+import java.util.HashMap;
 import java.util.Map;
 
 /**
  * Helper class to generate specific credential properties for different table 
format and engine.
  */
 public class CredentialPropertyUtils {
+  private static Map<String, String> icebergCredentialPropertyMap =
+      ImmutableMap.of(GCSTokenCredential.GCS_TOKEN_NAME, "gcs.oauth2.token");
+
   /**
    * Transforms a specific credential into a map of Iceberg properties.
    *
@@ -32,7 +37,25 @@ public class CredentialPropertyUtils {
    * @return a map of Iceberg properties derived from the credential
    */
   public static Map<String, String> toIcebergProperties(Credential credential) 
{
-    // todo: transform specific credential to iceberg properties
+    if (credential instanceof GCSTokenCredential) {
+      Map<String, String> icebergGCSCredentialProperties =
+          transformProperties(credential.credentialInfo(), 
icebergCredentialPropertyMap);
+      icebergGCSCredentialProperties.put(
+          "gcs.oauth2.token-expires-at", 
String.valueOf(credential.expireTimeInMs()));
+      return icebergGCSCredentialProperties;
+    }
     return credential.toProperties();
   }
+
+  private static Map<String, String> transformProperties(
+      Map<String, String> originProperties, Map<String, String> transformMap) {
+    HashMap<String, String> properties = new HashMap();
+    originProperties.forEach(
+        (k, v) -> {
+          if (transformMap.containsKey(k)) {
+            properties.put(transformMap.get(k), v);
+          }
+        });
+    return properties;
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/credential/config/GCSCredentialConfig.java
 
b/core/src/main/java/org/apache/gravitino/credential/config/GCSCredentialConfig.java
new file mode 100644
index 000000000..1a2b38ef6
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/credential/config/GCSCredentialConfig.java
@@ -0,0 +1,51 @@
+/*
+ *  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 com.google.common.annotations.VisibleForTesting;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.config.ConfigBuilder;
+import org.apache.gravitino.config.ConfigConstants;
+import org.apache.gravitino.config.ConfigEntry;
+
+public class GCSCredentialConfig extends Config {
+
+  @VisibleForTesting
+  public static final String GRAVITINO_GCS_CREDENTIAL_FILE_PATH = 
"gcs-credential-file-path";
+
+  public static final ConfigEntry<String> GCS_CREDENTIAL_FILE_PATH =
+      new ConfigBuilder(GRAVITINO_GCS_CREDENTIAL_FILE_PATH)
+          .doc("The path of GCS credential file")
+          .version(ConfigConstants.VERSION_0_7_0)
+          .stringConf()
+          .create();
+
+  public GCSCredentialConfig(Map<String, String> properties) {
+    super(false);
+    loadFromMap(properties, k -> true);
+  }
+
+  @Nullable
+  public String gcsCredentialFilePath() {
+    return this.get(GCS_CREDENTIAL_FILE_PATH);
+  }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 472be136c..830fe5e74 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -107,6 +107,7 @@ datanucleus-api-jdo = "4.2.4"
 datanucleus-rdbms = "4.1.19"
 datanucleus-jdo = "3.2.0-m3"
 hudi = "0.15.0"
+google-auth = "1.28.0"
 
 [libraries]
 protobuf-java = { group = "com.google.protobuf", name = "protobuf-java", 
version.ref = "protoc" }
@@ -180,6 +181,7 @@ iceberg-core = { group = "org.apache.iceberg", name = 
"iceberg-core", version.re
 iceberg-api = { group = "org.apache.iceberg", name = "iceberg-api", 
version.ref = "iceberg" }
 iceberg-hive-metastore = { group = "org.apache.iceberg", name = 
"iceberg-hive-metastore", version.ref = "iceberg" }
 iceberg-gcp = { group = "org.apache.iceberg", name = "iceberg-gcp", 
version.ref = "iceberg" }
+iceberg-gcp-bundle = { group = "org.apache.iceberg", name = 
"iceberg-gcp-bundle", version.ref = "iceberg" }
 paimon-core = { group = "org.apache.paimon", name = "paimon-core", version.ref 
= "paimon" }
 paimon-format = { group = "org.apache.paimon", name = "paimon-format", 
version.ref = "paimon" }
 paimon-hive-catalog = { group = "org.apache.paimon", name = 
"paimon-hive-catalog", version.ref = "paimon" }
@@ -246,6 +248,9 @@ mail = { group = "javax.mail", name = "mail", version.ref = 
"mail" }
 rome = { group = "rome", name = "rome", version.ref = "rome" }
 jettison = { group = "org.codehaus.jettison", name = "jettison", version.ref = 
"jettison" }
 
+google-auth-http = { group = "com.google.auth", name = 
"google-auth-library-oauth2-http", version.ref = "google-auth" }
+google-auth-credentials = { group = "com.google.auth", name = 
"google-auth-library-credentials", version.ref = "google-auth" }
+
 [bundles]
 log4j = ["slf4j-api", "log4j-slf4j2-impl", "log4j-api", "log4j-core", 
"log4j-12-api"]
 jetty = ["jetty-server", "jetty-servlet", "jetty-webapp", "jetty-servlets"]
diff --git a/iceberg/iceberg-rest-server/build.gradle.kts 
b/iceberg/iceberg-rest-server/build.gradle.kts
index 594e6d042..f088ce292 100644
--- a/iceberg/iceberg-rest-server/build.gradle.kts
+++ b/iceberg/iceberg-rest-server/build.gradle.kts
@@ -63,6 +63,7 @@ dependencies {
 
   compileOnly(libs.lombok)
 
+  testImplementation(project(":bundles:gcp-bundle", configuration = "shadow"))
   testImplementation(project(":integration-test-common", "testArtifacts"))
 
   
testImplementation("org.scala-lang.modules:scala-collection-compat_$scalaVersion:$scalaCollectionCompatVersion")
@@ -75,6 +76,7 @@ dependencies {
     exclude("org.rocksdb")
   }
 
+  testImplementation(libs.iceberg.gcp.bundle)
   testImplementation(libs.jersey.test.framework.core) {
     exclude(group = "org.junit.jupiter")
   }
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTGCSIT.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTGCSIT.java
new file mode 100644
index 000000000..89f56c517
--- /dev/null
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTGCSIT.java
@@ -0,0 +1,108 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package org.apache.gravitino.iceberg.integration.test;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergConstants;
+import org.apache.gravitino.credential.CredentialConstants;
+import org.apache.gravitino.credential.config.GCSCredentialConfig;
+import org.apache.gravitino.iceberg.common.IcebergConfig;
+import org.apache.gravitino.integration.test.util.BaseIT;
+import org.apache.gravitino.integration.test.util.DownloaderUtils;
+import org.apache.gravitino.integration.test.util.ITUtils;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+
+// You should export GRAVITINO_GCS_BUCKET and GOOGLE_APPLICATION_CREDENTIALS 
to run the test
+@EnabledIfEnvironmentVariable(named = "GRAVITINO_TEST_CLOUD_IT", matches = 
"true")
+public class IcebergRESTGCSIT extends IcebergRESTJdbcCatalogIT {
+  private String gcsWarehouse;
+  private String gcsCredentialPath;
+
+  @Override
+  void initEnv() {
+    this.gcsWarehouse =
+        String.format("gs://%s/test", 
getFromEnvOrDefault("GRAVITINO_GCS_BUCKET", "bucketName"));
+    this.gcsCredentialPath =
+        getFromEnvOrDefault("GOOGLE_APPLICATION_CREDENTIALS", 
"credential.json");
+    if (ITUtils.isEmbedded()) {
+      return;
+    }
+
+    try {
+      downloadIcebergBundleJar();
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+    copyGCSBundleJar();
+  }
+
+  @Override
+  public Map<String, String> getCatalogConfig() {
+    HashMap m = new HashMap<String, String>();
+    m.putAll(getCatalogJdbcConfig());
+    m.putAll(getGCSConfig());
+    return m;
+  }
+
+  public boolean supportsCredentialVending() {
+    return true;
+  }
+
+  private Map<String, String> getGCSConfig() {
+    Map configMap = new HashMap<String, String>();
+
+    configMap.put(
+        IcebergConfig.ICEBERG_CONFIG_PREFIX + 
CredentialConstants.CREDENTIAL_PROVIDER_TYPE,
+        CredentialConstants.GCS_TOKEN_CREDENTIAL_PROVIDER_TYPE);
+    configMap.put(
+        IcebergConfig.ICEBERG_CONFIG_PREFIX
+            + GCSCredentialConfig.GRAVITINO_GCS_CREDENTIAL_FILE_PATH,
+        gcsCredentialPath);
+    configMap.put(
+        IcebergConfig.ICEBERG_CONFIG_PREFIX + IcebergConstants.IO_IMPL,
+        "org.apache.iceberg.gcp.gcs.GCSFileIO");
+    configMap.put(IcebergConfig.ICEBERG_CONFIG_PREFIX + 
IcebergConstants.WAREHOUSE, gcsWarehouse);
+    return configMap;
+  }
+
+  private void copyGCSBundleJar() {
+    String gravitinoHome = System.getenv("GRAVITINO_HOME");
+    String targetDir = String.format("%s/iceberg-rest-server/libs/", 
gravitinoHome);
+    BaseIT.copyBundleJarsToDirectory("gcp-bundle", targetDir);
+  }
+
+  private void downloadIcebergBundleJar() throws IOException {
+    String icebergBundleJarName = "iceberg-gcp-bundle-1.5.2.jar";
+    String icebergBundleJarUri =
+        
"https://repo1.maven.org/maven2/org/apache/iceberg/iceberg-gcp-bundle/1.5.2/";
+            + icebergBundleJarName;
+    String gravitinoHome = System.getenv("GRAVITINO_HOME");
+    String targetDir = String.format("%s/iceberg-rest-server/libs/", 
gravitinoHome);
+    DownloaderUtils.downloadFile(icebergBundleJarUri, targetDir);
+  }
+
+  private String getFromEnvOrDefault(String envVar, String defaultValue) {
+    String envValue = System.getenv(envVar);
+    return Optional.ofNullable(envValue).orElse(defaultValue);
+  }
+}
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTJdbcCatalogIT.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTJdbcCatalogIT.java
index d53f80220..c235451f2 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTJdbcCatalogIT.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTJdbcCatalogIT.java
@@ -33,7 +33,9 @@ import org.junit.jupiter.api.TestInstance.Lifecycle;
 @Tag("gravitino-docker-test")
 @TestInstance(Lifecycle.PER_CLASS)
 public class IcebergRESTJdbcCatalogIT extends IcebergRESTServiceIT {
+
   private static final ContainerSuite containerSuite = 
ContainerSuite.getInstance();
+  private boolean hiveStarted = false;
 
   public IcebergRESTJdbcCatalogIT() {
     catalogType = IcebergCatalogBackend.JDBC;
@@ -42,9 +44,15 @@ public class IcebergRESTJdbcCatalogIT extends 
IcebergRESTServiceIT {
   @Override
   void initEnv() {
     containerSuite.startHiveContainer();
+    hiveStarted = true;
   }
 
+  @Override
   public Map<String, String> getCatalogConfig() {
+    return getCatalogJdbcConfig();
+  }
+
+  protected Map<String, String> getCatalogJdbcConfig() {
     Map<String, String> configMap = new HashMap<>();
 
     configMap.put(
@@ -70,13 +78,15 @@ public class IcebergRESTJdbcCatalogIT extends 
IcebergRESTServiceIT {
 
     configMap.put(IcebergConfig.ICEBERG_CONFIG_PREFIX + "jdbc.schema-version", 
"V1");
 
-    configMap.put(
-        IcebergConfig.ICEBERG_CONFIG_PREFIX + 
IcebergConfig.CATALOG_WAREHOUSE.getKey(),
-        GravitinoITUtils.genRandomName(
-            String.format(
-                "hdfs://%s:%d/user/hive/warehouse-jdbc-sqlite",
-                containerSuite.getHiveContainer().getContainerIpAddress(),
-                HiveContainer.HDFS_DEFAULTFS_PORT)));
+    if (hiveStarted) {
+      configMap.put(
+          IcebergConfig.ICEBERG_CONFIG_PREFIX + 
IcebergConfig.CATALOG_WAREHOUSE.getKey(),
+          GravitinoITUtils.genRandomName(
+              String.format(
+                  "hdfs://%s:%d/user/hive/warehouse-jdbc-sqlite",
+                  containerSuite.getHiveContainer().getContainerIpAddress(),
+                  HiveContainer.HDFS_DEFAULTFS_PORT)));
+    }
     return configMap;
   }
 }
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTServiceBaseIT.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTServiceBaseIT.java
index 0ba781cab..67e7a3b8f 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTServiceBaseIT.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTServiceBaseIT.java
@@ -20,6 +20,8 @@ package org.apache.gravitino.iceberg.integration.test;
 
 import com.google.common.collect.ImmutableList;
 import com.google.errorprone.annotations.FormatMethod;
+import java.io.File;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -27,11 +29,14 @@ import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
+import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.iceberg.common.IcebergCatalogBackend;
 import org.apache.gravitino.iceberg.common.IcebergConfig;
 import 
org.apache.gravitino.iceberg.integration.test.util.IcebergRESTServerManager;
+import org.apache.gravitino.integration.test.util.ITUtils;
 import org.apache.gravitino.server.web.JettyServerConfig;
+import org.apache.spark.SparkConf;
 import org.apache.spark.sql.Row;
 import org.apache.spark.sql.SparkSession;
 import org.junit.jupiter.api.AfterAll;
@@ -46,6 +51,7 @@ import org.slf4j.LoggerFactory;
 
 @SuppressWarnings("FormatStringAnnotation")
 public abstract class IcebergRESTServiceBaseIT {
+
   public static final Logger LOG = 
LoggerFactory.getLogger(IcebergRESTServiceBaseIT.class);
   private SparkSession sparkSession;
   protected IcebergCatalogBackend catalogType = IcebergCatalogBackend.MEMORY;
@@ -84,6 +90,31 @@ public abstract class IcebergRESTServiceBaseIT {
 
   abstract Map<String, String> getCatalogConfig();
 
+  protected boolean supportsCredentialVending() {
+    return false;
+  }
+
+  private void copyBundleJar(String bundleName) {
+    String bundleFileName = ITUtils.getBundleJarName(bundleName);
+
+    String rootDir = System.getenv("GRAVITINO_ROOT_DIR");
+    String sourceFile =
+        String.format("%s/bundles/gcp-bundle/build/libs/%s", rootDir, 
bundleFileName);
+    String gravitinoHome = System.getenv("GRAVITINO_HOME");
+    String targetDir = String.format("%s/iceberg-rest-server/libs/", 
gravitinoHome);
+    String targetFile = String.format("%s/%s", targetDir, bundleFileName);
+    LOG.info("Source file: {}, target directory: {}", sourceFile, targetDir);
+    try {
+      File target = new File(targetFile);
+      if (!target.exists()) {
+        LOG.info("Copy source file: {} to target directory: {}", sourceFile, 
targetDir);
+        FileUtils.copyFileToDirectory(new File(sourceFile), new 
File(targetDir));
+      }
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
   private void registerIcebergCatalogConfig() {
     Map<String, String> icebergConfigs = getCatalogConfig();
     icebergRESTServerManager.registerCustomConfigs(icebergConfigs);
@@ -100,19 +131,24 @@ public abstract class IcebergRESTServiceBaseIT {
   private void initSparkEnv() {
     int port = getServerPort();
     LOG.info("Iceberg REST server port:{}", port);
-    String IcebergRESTUri = String.format("http://127.0.0.1:%d/iceberg/";, 
port);
-    sparkSession =
-        SparkSession.builder()
-            .master("local[1]")
-            .config(
+    String icebergRESTUri = String.format("http://127.0.0.1:%d/iceberg/";, 
port);
+    SparkConf sparkConf =
+        new SparkConf()
+            .set(
                 "spark.sql.extensions",
                 
"org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions")
-            .config("spark.sql.catalog.rest", 
"org.apache.iceberg.spark.SparkCatalog")
-            .config("spark.sql.catalog.rest.type", "rest")
-            .config("spark.sql.catalog.rest.uri", IcebergRESTUri)
+            .set("spark.sql.catalog.rest", 
"org.apache.iceberg.spark.SparkCatalog")
+            .set("spark.sql.catalog.rest.type", "rest")
+            .set("spark.sql.catalog.rest.uri", icebergRESTUri)
             // drop Iceberg table purge may hang in spark local mode
-            .config("spark.locality.wait.node", "0")
-            .getOrCreate();
+            .set("spark.locality.wait.node", "0");
+
+    if (supportsCredentialVending()) {
+      sparkConf.set(
+          "spark.sql.catalog.rest.header.X-Iceberg-Access-Delegation", 
"vended-credentials");
+    }
+
+    sparkSession = 
SparkSession.builder().master("local[1]").config(sparkConf).getOrCreate();
   }
 
   private void stopSparkEnv() {
diff --git 
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/BaseIT.java
 
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/BaseIT.java
index 8bbb5a3b2..e7ed483f2 100644
--- 
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/BaseIT.java
+++ 
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/BaseIT.java
@@ -75,6 +75,7 @@ import org.testcontainers.shaded.org.awaitility.Awaitility;
 @ExtendWith({PrintFuncNameExtension.class, CloseContainerExtension.class})
 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
 public class BaseIT {
+
   protected static final ContainerSuite containerSuite = 
ContainerSuite.getInstance();
 
   private static final Logger LOG = LoggerFactory.getLogger(BaseIT.class);
@@ -127,7 +128,9 @@ public class BaseIT {
       originConfig = FileUtils.readFileToString(configPath.toFile(), 
StandardCharsets.UTF_8);
     }
 
-    if (customConfigs.isEmpty()) return;
+    if (customConfigs.isEmpty()) {
+      return;
+    }
 
     String tmpFileName = GravitinoServer.CONF_FILE + ".tmp";
     Path tmpPath = Paths.get(gravitinoHome, "conf", tmpFileName);
@@ -397,26 +400,26 @@ public class BaseIT {
     return Objects.equals(mode, ITUtils.DEPLOY_TEST_MODE);
   }
 
-  protected void copyBundleJarsToHadoop(String bundleName) {
+  public static void copyBundleJarsToDirectory(String bundleName, String 
directory) {
+    String bundleJarSourceFile = ITUtils.getBundleJarSourceFile(bundleName);
+    try {
+      DownloaderUtils.downloadFile(bundleJarSourceFile, directory);
+    } catch (Exception e) {
+      throw new RuntimeException(
+          String.format(
+              "Failed to copy the %s dependency jars: %s to %s",
+              bundleName, bundleJarSourceFile, directory),
+          e);
+    }
+  }
+
+  protected static void copyBundleJarsToHadoop(String bundleName) {
     if (!isDeploy()) {
       return;
     }
 
     String gravitinoHome = System.getenv("GRAVITINO_HOME");
-    String jarName =
-        String.format("gravitino-%s-%s.jar", bundleName, 
System.getenv("PROJECT_VERSION"));
-    String gcsJars =
-        ITUtils.joinPath(
-            gravitinoHome, "..", "..", "bundles", bundleName, "build", "libs", 
jarName);
-    gcsJars = "file://" + gcsJars;
-    try {
-      if (!ITUtils.EMBEDDED_TEST_MODE.equals(testMode)) {
-        String hadoopLibDirs = ITUtils.joinPath(gravitinoHome, "catalogs", 
"hadoop", "libs");
-        DownloaderUtils.downloadFile(gcsJars, hadoopLibDirs);
-      }
-    } catch (Exception e) {
-      throw new RuntimeException(
-          String.format("Failed to copy the %s dependency jars: %s", 
bundleName, gcsJars), e);
-    }
+    String hadoopLibDirs = ITUtils.joinPath(gravitinoHome, "catalogs", 
"hadoop", "libs");
+    copyBundleJarsToDirectory(bundleName, hadoopLibDirs);
   }
 }
diff --git 
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/ITUtils.java
 
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/ITUtils.java
index 9a6d7b130..d7c099dc7 100644
--- 
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/ITUtils.java
+++ 
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/ITUtils.java
@@ -48,6 +48,7 @@ import org.apache.gravitino.rel.partitions.Partition;
 import org.junit.jupiter.api.Assertions;
 
 public class ITUtils {
+
   public static final String TEST_MODE = "testMode";
   public static final String EMBEDDED_TEST_MODE = "embedded";
   public static final String DEPLOY_TEST_MODE = "deploy";
@@ -186,5 +187,20 @@ public class ITUtils {
     return Objects.equals(mode, ITUtils.EMBEDDED_TEST_MODE);
   }
 
+  public static String getBundleJarSourceFile(String bundleName) {
+    String jarName = ITUtils.getBundleJarName(bundleName);
+    String gcsJars = 
ITUtils.joinPath(ITUtils.getBundleJarDirectory(bundleName), jarName);
+    return "file://" + gcsJars;
+  }
+
+  public static String getBundleJarName(String bundleName) {
+    return String.format("gravitino-%s-%s.jar", bundleName, 
System.getenv("PROJECT_VERSION"));
+  }
+
+  public static String getBundleJarDirectory(String bundleName) {
+    return ITUtils.joinPath(
+        System.getenv("GRAVITINO_ROOT_DIR"), "bundles", bundleName, "build", 
"libs");
+  }
+
   private ITUtils() {}
 }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 2eb340baa..1f3efb495 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -73,5 +73,4 @@ include("docs")
 include("integration-test-common")
 include(":bundles:aws-bundle")
 include(":bundles:gcp-bundle")
-include("bundles:aliyun-bundle")
-findProject(":bundles:aliyun-bundle")?.name = "aliyun-bundle"
+include(":bundles:aliyun-bundle")


Reply via email to