This is an automated email from the ASF dual-hosted git repository. diqiu50 pushed a commit to branch trino-irc-1.3 in repository https://gitbox.apache.org/repos/asf/gravitino.git
commit b3eef41feafe4d1f936a674cc4f5f86919478def Author: diqiu50 <[email protected]> AuthorDate: Tue Aug 25 17:46:11 2026 +0800 improvement(spark-connector): Route lakehouse-iceberg catalogs through the Iceberg REST server Mirrors the Trino connector's IRC routing: hive/jdbc backed lakehouse-iceberg catalogs are now automatically routed through Gravitino's Iceberg REST server when the server exposes a discoverable endpoint for the metalake, gaining vended-credentials support instead of static long-lived secrets. Manual override and REST client auth pass-through are configurable via spark.sql.gravitino.iceberg.rest-uri and spark.sql.gravitino.iceberg.rest.. --- docs/spark-connector/spark-catalog-iceberg.md | 40 +++++ docs/spark-connector/spark-connector.md | 2 + .../spark/connector/GravitinoSparkConfig.java | 9 ++ .../connector/catalog/GravitinoCatalogManager.java | 45 +++++- .../connector/iceberg/GravitinoIcebergCatalog.java | 73 ++++++++- .../iceberg/IcebergPropertiesConstants.java | 17 ++ .../iceberg/IcebergPropertiesConverter.java | 176 +++++++++++++++++++++ .../connector/plugin/GravitinoDriverPlugin.java | 1 + .../connector/glue/TestGravitinoGlueCatalog.java | 2 +- .../iceberg/TestIcebergPropertiesConverter.java | 136 ++++++++++++++++ .../connector/integration/test/SparkEnvIT.java | 59 ++++--- .../iceberg/SparkIcebergCatalogRestRoutingIT.java | 76 +++++++++ .../SparkIcebergCatalogRestRoutingIT33.java | 62 ++++++++ .../SparkIcebergCatalogRestRoutingIT34.java | 25 +++ .../SparkIcebergCatalogRestRoutingIT35.java | 25 +++ 15 files changed, 722 insertions(+), 26 deletions(-) diff --git a/docs/spark-connector/spark-catalog-iceberg.md b/docs/spark-connector/spark-catalog-iceberg.md index fd3210e4eb..ee8a5080a1 100644 --- a/docs/spark-connector/spark-catalog-iceberg.md +++ b/docs/spark-connector/spark-catalog-iceberg.md @@ -148,6 +148,46 @@ Gravitino catalog property names with the prefix `spark.bypass.` are passed to S Iceberg catalog property `cache-enabled` is setting to `false` internally and not allowed to change. ::: +## Routing Through the Gravitino Iceberg REST Server + +If the Gravitino server exposes an [Iceberg REST catalog](../iceberg-rest-service.md) (IRC) endpoint for the +current metalake, the Spark connector automatically routes `hive` and `jdbc` backed Iceberg catalogs through +that endpoint instead of talking to the Hive metastore or JDBC database directly. This has no effect on +catalogs whose `catalog-backend` is already `rest` or `custom`. + +Routing through the IRC server is the only way to receive short-lived, per-table **vended credentials** +instead of the static, long-lived secrets (JDBC password, S3/OSS access keys) that would otherwise be +placed in the Spark Iceberg connector configuration. See [Credential vending](../security/credential-vending.md) +for how to enable credential vending on the Gravitino server. + +The endpoint is discovered once, when a catalog is first initialized in a Spark session; it is not +re-checked afterward. If the server does not expose a discoverable endpoint (for example, the `iceberg-rest` +auxiliary service is disabled, or not configured with `catalog-config-provider=dynamic-config-provider`), the +catalog falls back to the existing hive/jdbc translation described above. + +To force a specific endpoint instead of relying on auto-discovery, set: + +```properties +spark.sql.gravitino.iceberg.rest-uri http://<gravitino-host>:9001/iceberg +``` + +If Gravitino requires authentication on the IRC endpoint, pass the Iceberg REST client's own auth +properties using the `spark.sql.gravitino.iceberg.rest.` prefix. For example, for Basic authentication: + +```properties +spark.sql.gravitino.iceberg.rest.rest.auth.type basic +spark.sql.gravitino.iceberg.rest.rest.auth.basic.username <username> +spark.sql.gravitino.iceberg.rest.rest.auth.basic.password <password> +``` + +See [Connect Spark to Iceberg REST](../iceberg-rest-engine/spark.md) for the full set of supported +`rest.auth.*` properties and how to configure them when connecting directly to the IRC endpoint. + +Because vended credentials are only consumed by Iceberg's native `FileIO` implementations, make sure the +warehouse storage jars listed under [Storage](#storage) below are on the Spark classpath; the connector +derives `io-impl` automatically from the warehouse location's scheme (`s3`/`s3a`/`s3n`, `gs`, +`abfs`/`abfss`/`wasb`/`wasbs`) unless `io-impl` is already set explicitly on the catalog. + ## Storage Spark connector could convert storage properties in the Gravitino catalog to Spark Iceberg connector automatically, No extra configuration is needed for `S3`, `ADLS`, `OSS`, `GCS`. diff --git a/docs/spark-connector/spark-connector.md b/docs/spark-connector/spark-connector.md index 5a3d8562c1..8f24b1d9b3 100644 --- a/docs/spark-connector/spark-connector.md +++ b/docs/spark-connector/spark-connector.md @@ -34,6 +34,8 @@ The Apache Gravitino Spark connector leverages the Spark DataSourceV2 interface | spark.sql.gravitino.enableIcebergSupport | string | `false` | Set to `true` to use Iceberg catalog. | No | | spark.sql.gravitino.enablePaimonSupport | string | `false` | Set to `true` to use Paimon catalog. | No | | spark.sql.gravitino.client. | string | (none) | The configuration key prefix for the Gravitino client config. | No | +| spark.sql.gravitino.iceberg.rest-uri | string | (none) | Overrides the auto-discovered Gravitino Iceberg REST server endpoint. See [Iceberg catalog](spark-catalog-iceberg.md#routing-through-the-gravitino-iceberg-rest-server). | No | +| spark.sql.gravitino.iceberg.rest. | string | (none) | The configuration key prefix for the Iceberg REST client config (e.g. `rest.auth.type`), applied when a catalog is routed through the Gravitino Iceberg REST server. | No | To configure the Gravitino client, use properties prefixed with `spark.sql.gravitino.client.`. These properties will be passed to the Gravitino client after removing the `spark.sql.` prefix. diff --git a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/GravitinoSparkConfig.java b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/GravitinoSparkConfig.java index be108c9af1..78f8de661e 100644 --- a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/GravitinoSparkConfig.java +++ b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/GravitinoSparkConfig.java @@ -32,6 +32,15 @@ public class GravitinoSparkConfig { GRAVITINO_PREFIX + "enablePaimonSupport"; public static final String GRAVITINO_CLIENT_CONFIG_PREFIX = GRAVITINO_PREFIX + "client."; + // Manually overrides the Gravitino Iceberg REST server endpoint that hive/jdbc backed + // lakehouse-iceberg catalogs are routed through; takes precedence over auto-discovery. + public static final String GRAVITINO_ICEBERG_REST_URI = GRAVITINO_PREFIX + "iceberg.rest-uri"; + + // Pass-through prefix for the Iceberg REST client config (e.g. rest.auth.type, + // rest.auth.basic.username), applied when a catalog is routed through the Iceberg REST server. + public static final String GRAVITINO_ICEBERG_REST_CONFIG_PREFIX = + GRAVITINO_PREFIX + "iceberg.rest."; + public static final String GRAVITINO_AUTH_TYPE = GRAVITINO_PREFIX + AuthProperties.GRAVITINO_CLIENT_AUTH_TYPE; public static final String GRAVITINO_OAUTH2_URI = diff --git a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/catalog/GravitinoCatalogManager.java b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/catalog/GravitinoCatalogManager.java index a484972656..0d92b96ee8 100644 --- a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/catalog/GravitinoCatalogManager.java +++ b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/catalog/GravitinoCatalogManager.java @@ -24,6 +24,7 @@ import com.google.common.base.Preconditions; import com.google.common.base.Supplier; import java.util.Arrays; import java.util.Map; +import java.util.Optional; import org.apache.gravitino.Catalog; import org.apache.gravitino.client.GravitinoClient; import org.slf4j.Logger; @@ -37,17 +38,24 @@ public class GravitinoCatalogManager { private volatile boolean isClosed = false; private final Cache<String, Catalog> gravitinoCatalogs; private final GravitinoClient gravitinoClient; + private final String metalakeName; - private GravitinoCatalogManager(Supplier<GravitinoClient> clientBuilder) { + // Resolved lazily on first access and cached for the life of this manager; Spark catalogs are + // initialized once, so there is no refresh path if the Iceberg REST endpoint changes later. + private volatile Optional<String> icebergRestUri; + + private GravitinoCatalogManager(String metalakeName, Supplier<GravitinoClient> clientBuilder) { + this.metalakeName = metalakeName; this.gravitinoClient = clientBuilder.get(); // Will not evict catalog by default this.gravitinoCatalogs = Caffeine.newBuilder().build(); } - public static GravitinoCatalogManager create(Supplier<GravitinoClient> clientBuilder) { + public static GravitinoCatalogManager create( + String metalakeName, Supplier<GravitinoClient> clientBuilder) { Preconditions.checkState( gravitinoCatalogManager == null, "Should not create duplicate GravitinoCatalogManager"); - gravitinoCatalogManager = new GravitinoCatalogManager(clientBuilder); + gravitinoCatalogManager = new GravitinoCatalogManager(metalakeName, clientBuilder); return gravitinoCatalogManager; } @@ -86,6 +94,37 @@ public class GravitinoCatalogManager { return gravitinoCatalogs.asMap(); } + /** + * Resolves the Gravitino Iceberg REST server endpoint for this manager's metalake, if the server + * exposes one. The lookup is performed once and the result, including a lookup failure, is cached + * for the lifetime of this manager. + * + * @return the discovered Iceberg REST endpoint, or empty if none is available + */ + public Optional<String> getIcebergRestUri() { + if (icebergRestUri == null) { + synchronized (this) { + if (icebergRestUri == null) { + icebergRestUri = resolveIcebergRestUri(); + } + } + } + return icebergRestUri; + } + + private Optional<String> resolveIcebergRestUri() { + try { + return gravitinoClient.icebergRestServiceUri(metalakeName); + } catch (Exception e) { + LOG.debug( + "No Iceberg REST server endpoint is available for metalake {}, " + + "falling back to native catalog backend routing.", + metalakeName, + e); + return Optional.empty(); + } + } + private Catalog loadCatalog(String catalogName) { Catalog catalog = gravitinoClient.loadCatalog(catalogName); Preconditions.checkArgument( diff --git a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/iceberg/GravitinoIcebergCatalog.java b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/iceberg/GravitinoIcebergCatalog.java index 0fa99d3ab7..cb0615dfad 100644 --- a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/iceberg/GravitinoIcebergCatalog.java +++ b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/iceberg/GravitinoIcebergCatalog.java @@ -21,21 +21,29 @@ package org.apache.gravitino.spark.connector.iceberg; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Locale; import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergConstants; import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergPropertiesUtils; import org.apache.gravitino.credential.CredentialPropertyUtils; import org.apache.gravitino.rel.Table; +import org.apache.gravitino.spark.connector.GravitinoSparkConfig; import org.apache.gravitino.spark.connector.PropertiesConverter; import org.apache.gravitino.spark.connector.SparkTransformConverter; import org.apache.gravitino.spark.connector.SparkTypeConverter; import org.apache.gravitino.spark.connector.catalog.BaseCatalog; +import org.apache.gravitino.spark.connector.catalog.GravitinoCatalogManager; import org.apache.iceberg.catalog.Catalog; import org.apache.iceberg.spark.SparkCatalog; import org.apache.iceberg.spark.procedures.SparkProcedures; import org.apache.iceberg.spark.source.HasIcebergCatalog; import org.apache.iceberg.spark.source.SparkTable; +import org.apache.spark.sql.SparkSession; import org.apache.spark.sql.catalyst.analysis.NoSuchFunctionException; import org.apache.spark.sql.catalyst.analysis.NoSuchNamespaceException; import org.apache.spark.sql.catalyst.analysis.NoSuchProcedureException; @@ -72,15 +80,72 @@ public class GravitinoIcebergCatalog extends BaseCatalog } } String catalogBackendName = IcebergPropertiesUtils.getCatalogBackendName(properties); - Map<String, String> all = - getPropertiesConverter().toSparkCatalogProperties(options, properties); - CredentialPropertyUtils.applyIcebergCredentials( - CredentialPropertyUtils.getCredentials(gravitinoCatalogClient), all); + Optional<String> icebergRestUri = resolveIcebergRestUri(properties); + Map<String, String> all; + if (icebergRestUri.isPresent()) { + all = buildIcebergRestSparkCatalogProperties(name, options, properties, icebergRestUri.get()); + } else { + all = getPropertiesConverter().toSparkCatalogProperties(options, properties); + CredentialPropertyUtils.applyIcebergCredentials( + CredentialPropertyUtils.getCredentials(gravitinoCatalogClient), all); + } TableCatalog icebergCatalog = new SparkCatalog(); icebergCatalog.initialize(catalogBackendName, new CaseInsensitiveStringMap(all)); return icebergCatalog; } + /** + * Resolves the Iceberg REST server endpoint to route this catalog through, if any. Only hive/jdbc + * backed catalogs are eligible; a catalog already configured with {@code catalog-backend=rest} or + * {@code custom} is left untouched. + */ + private Optional<String> resolveIcebergRestUri(Map<String, String> properties) { + String backend = properties.get(IcebergConstants.CATALOG_BACKEND); + if (backend == null) { + return Optional.empty(); + } + String normalizedBackend = backend.toLowerCase(Locale.ROOT); + boolean eligible = + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND_HIVE.equals(normalizedBackend) + || IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND_JDBC.equals( + normalizedBackend); + if (!eligible) { + return Optional.empty(); + } + + String manualUri = + SparkSession.active().conf().get(GravitinoSparkConfig.GRAVITINO_ICEBERG_REST_URI, null); + if (StringUtils.isNotBlank(manualUri)) { + return Optional.of(manualUri); + } + return GravitinoCatalogManager.get().getIcebergRestUri(); + } + + private Map<String, String> buildIcebergRestSparkCatalogProperties( + String gravitinoCatalogName, + CaseInsensitiveStringMap options, + Map<String, String> properties, + String restUri) { + IcebergPropertiesConverter converter = (IcebergPropertiesConverter) getPropertiesConverter(); + Map<String, String> all = + new HashMap<>( + converter.buildIcebergRestProperties( + gravitinoCatalogName, restUri, properties, getIcebergRestClientConfig())); + if (options != null) { + all.putAll(options); + } + return all; + } + + private Map<String, String> getIcebergRestClientConfig() { + return Stream.of( + SparkSession.active() + .sparkContext() + .conf() + .getAllWithPrefix(GravitinoSparkConfig.GRAVITINO_ICEBERG_REST_CONFIG_PREFIX)) + .collect(Collectors.toMap(t -> t._1, t -> t._2, (oldVal, newVal) -> newVal)); + } + @Override protected org.apache.spark.sql.connector.catalog.Table createSparkTable( Identifier identifier, diff --git a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/iceberg/IcebergPropertiesConstants.java b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/iceberg/IcebergPropertiesConstants.java index 3d6ea418b4..71957d79da 100644 --- a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/iceberg/IcebergPropertiesConstants.java +++ b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/iceberg/IcebergPropertiesConstants.java @@ -81,5 +81,22 @@ public class IcebergPropertiesConstants { static final String GRAVITINO_ICEBERG_CATALOG_BACKEND_NAME = IcebergConstants.CATALOG_BACKEND_NAME; + // Only used when routing a hive/jdbc backed catalog through the Gravitino Iceberg REST server; + // not part of the Gravitino <-> Iceberg property mapping in IcebergPropertiesUtils. + static final String ICEBERG_REST_CATALOG_PREFIX = "prefix"; + + static final String ICEBERG_IO_IMPL = IcebergConstants.IO_IMPL; + static final String ICEBERG_S3_ENDPOINT = IcebergConstants.ICEBERG_S3_ENDPOINT; + static final String ICEBERG_S3_PATH_STYLE_ACCESS = IcebergConstants.ICEBERG_S3_PATH_STYLE_ACCESS; + static final String ICEBERG_AWS_S3_REGION = IcebergConstants.AWS_S3_REGION; + static final String ICEBERG_OSS_ENDPOINT = IcebergConstants.ICEBERG_OSS_ENDPOINT; + + static final String ICEBERG_S3_FILE_IO_IMPL = "org.apache.iceberg.aws.s3.S3FileIO"; + static final String ICEBERG_GCS_FILE_IO_IMPL = "org.apache.iceberg.gcp.gcs.GCSFileIO"; + static final String ICEBERG_ADLS_FILE_IO_IMPL = "org.apache.iceberg.azure.adlsv2.ADLSFileIO"; + + static final String ICEBERG_ACCESS_DELEGATION = IcebergConstants.ICEBERG_ACCESS_DELEGATION; + static final String ICEBERG_ACCESS_DELEGATION_VENDED_CREDENTIALS = "vended-credentials"; + private IcebergPropertiesConstants() {} } diff --git a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/iceberg/IcebergPropertiesConverter.java b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/iceberg/IcebergPropertiesConverter.java index 9f7011aed5..e9e2a8d118 100644 --- a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/iceberg/IcebergPropertiesConverter.java +++ b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/iceberg/IcebergPropertiesConverter.java @@ -20,17 +20,33 @@ package org.apache.gravitino.spark.connector.iceberg; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import java.util.HashMap; +import java.util.List; +import java.util.Locale; import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergCatalogBackend; import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergConstants; import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergPropertiesUtils; +import org.apache.gravitino.credential.CredentialConstants; import org.apache.gravitino.spark.connector.PropertiesConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** Transform Apache Iceberg catalog properties between Apache Spark and Apache Gravitino. */ public class IcebergPropertiesConverter implements PropertiesConverter { + private static final Logger LOG = LoggerFactory.getLogger(IcebergPropertiesConverter.class); + + /** Routing keys the connector always derives itself; they cannot be overridden. */ + private static final List<String> RESERVED_REST_PROPERTIES = + ImmutableList.of( + IcebergPropertiesConstants.ICEBERG_CATALOG_TYPE, + IcebergPropertiesConstants.ICEBERG_CATALOG_URI, + IcebergPropertiesConstants.ICEBERG_CATALOG_WAREHOUSE, + IcebergPropertiesConstants.ICEBERG_REST_CATALOG_PREFIX); + public static class IcebergPropertiesConverterHolder { private static final IcebergPropertiesConverter INSTANCE = new IcebergPropertiesConverter(); } @@ -68,6 +84,166 @@ public class IcebergPropertiesConverter implements PropertiesConverter { return all; } + /** + * Builds Spark Iceberg catalog properties that route requests through the Gravitino Iceberg REST + * server, regardless of the catalog's actual storage backend (hive/jdbc). This is the only path + * on which temporary credentials work: the Iceberg REST protocol vends a fresh credential per + * table access, so static secrets are intentionally not carried over from {@code + * gravitinoProperties}. Static-key credentials for the non-REST path are unaffected; they are + * still injected via {@link org.apache.gravitino.credential.CredentialPropertyUtils}. + * + * @param gravitinoCatalogName the Gravitino catalog name, used as both {@code warehouse} (for the + * initial catalog discovery request) and {@code prefix} (for every subsequent request path + * segment) so the REST server resolves the same Gravitino catalog + * @param restUri the Iceberg REST server endpoint to route through, resolved by the caller + * @param gravitinoProperties the Gravitino catalog properties + * @param icebergRestClientConfig operator-level Iceberg REST client config (e.g. {@code + * rest.auth.type}), applied after catalog-level {@code spark.bypass.} overrides since it is a + * cluster-wide operational setting + * @return the Spark Iceberg catalog properties + */ + Map<String, String> buildIcebergRestProperties( + String gravitinoCatalogName, + String restUri, + Map<String, String> gravitinoProperties, + Map<String, String> icebergRestClientConfig) { + Preconditions.checkArgument(StringUtils.isNotBlank(restUri), "restUri should not be blank"); + + Map<String, String> all = new HashMap<>(); + // Later put/putAll calls override earlier ones for the same key; this is call-order + // precedence, unrelated to HashMap's (unspecified) iteration order. + all.putAll(buildStorageProperties(gravitinoProperties)); + all.put( + IcebergPropertiesConstants.ICEBERG_ACCESS_DELEGATION, + IcebergPropertiesConstants.ICEBERG_ACCESS_DELEGATION_VENDED_CREDENTIALS); + // The catalog's own spark.bypass properties override the defaults above, so a renamed + // Iceberg client property can be worked around without a connector change. + all.putAll(extractSparkBypassProperties(gravitinoProperties)); + all.putAll(icebergRestClientConfig); + + warnOnReservedRestPropertyOverrides(gravitinoCatalogName, all); + + all.put( + IcebergPropertiesConstants.ICEBERG_CATALOG_TYPE, + IcebergPropertiesConstants.ICEBERG_CATALOG_BACKEND_REST); + all.put(IcebergPropertiesConstants.ICEBERG_CATALOG_URI, restUri); + all.put(IcebergPropertiesConstants.ICEBERG_CATALOG_WAREHOUSE, gravitinoCatalogName); + all.put(IcebergPropertiesConstants.ICEBERG_REST_CATALOG_PREFIX, gravitinoCatalogName); + all.put(IcebergPropertiesConstants.ICEBERG_CATALOG_CACHE_ENABLED, "FALSE"); + return all; + } + + private Map<String, String> extractSparkBypassProperties(Map<String, String> properties) { + Map<String, String> bypass = new HashMap<>(); + if (properties != null) { + properties.forEach( + (k, v) -> { + if (k.startsWith(SPARK_PROPERTY_PREFIX)) { + bypass.put(k.substring(SPARK_PROPERTY_PREFIX.length()), v); + } + }); + } + return bypass; + } + + private void warnOnReservedRestPropertyOverrides( + String gravitinoCatalogName, Map<String, String> config) { + for (String reserved : RESERVED_REST_PROPERTIES) { + if (config.containsKey(reserved)) { + LOG.info( + "Property '{}' set on catalog '{}' is ignored; the connector always derives it when " + + "routing through the Iceberg REST server.", + reserved, + gravitinoCatalogName); + } + } + } + + /** + * Derives the storage-related Iceberg client config from the catalog's warehouse location. Only + * non-secret settings (native FileIO impl, custom endpoint, region, path-style access) are + * carried over; static access keys and JDBC credentials are deliberately excluded since the REST + * path vends its own temporary credentials. + */ + private Map<String, String> buildStorageProperties(Map<String, String> gravitinoProperties) { + Map<String, String> icebergProperties = + IcebergPropertiesUtils.toIcebergCatalogProperties(gravitinoProperties); + Map<String, String> storageProperties = new HashMap<>(); + copyIfPresent(icebergProperties, IcebergPropertiesConstants.ICEBERG_IO_IMPL, storageProperties); + + String warehouse = gravitinoProperties.get(IcebergConstants.WAREHOUSE); + String fileIoImpl = deriveFileIoImpl(warehouse); + if (fileIoImpl != null) { + storageProperties.putIfAbsent(IcebergPropertiesConstants.ICEBERG_IO_IMPL, fileIoImpl); + } else { + warnOnSchemeWithoutNativeFileIo(gravitinoProperties, warehouse); + } + + copyIfPresent( + icebergProperties, IcebergPropertiesConstants.ICEBERG_S3_ENDPOINT, storageProperties); + copyIfPresent( + icebergProperties, IcebergPropertiesConstants.ICEBERG_AWS_S3_REGION, storageProperties); + copyIfPresent( + icebergProperties, + IcebergPropertiesConstants.ICEBERG_S3_PATH_STYLE_ACCESS, + storageProperties); + copyIfPresent( + icebergProperties, IcebergPropertiesConstants.ICEBERG_OSS_ENDPOINT, storageProperties); + return storageProperties; + } + + private String deriveFileIoImpl(String warehouse) { + if (StringUtils.isBlank(warehouse) || !warehouse.contains("://")) { + return null; + } + String scheme = StringUtils.substringBefore(warehouse, "://").toLowerCase(Locale.ROOT); + switch (scheme) { + case "s3": + case "s3a": + case "s3n": + return IcebergPropertiesConstants.ICEBERG_S3_FILE_IO_IMPL; + case "gs": + return IcebergPropertiesConstants.ICEBERG_GCS_FILE_IO_IMPL; + case "abfs": + case "abfss": + case "wasb": + case "wasbs": + return IcebergPropertiesConstants.ICEBERG_ADLS_FILE_IO_IMPL; + default: + return null; + } + } + + /** + * Warns when a warehouse scheme has no native Iceberg FileIO but the catalog vends credentials. + * Vended credentials are only consumed by Iceberg's native FileIO implementations, so table + * access would otherwise fail at read time with a storage authentication error far from its + * cause, unless {@code io-impl} is set explicitly on the catalog. + */ + private void warnOnSchemeWithoutNativeFileIo( + Map<String, String> gravitinoProperties, String warehouse) { + if (StringUtils.isBlank(warehouse) || !warehouse.contains("://")) { + // hdfs and file legitimately have no native FileIO here, and neither vends credentials. + return; + } + if (StringUtils.isBlank(gravitinoProperties.get(CredentialConstants.CREDENTIAL_PROVIDERS))) { + return; + } + LOG.warn( + "Warehouse '{}' has no native Iceberg FileIO for the credentials vended by '{}' to be " + + "applied to; table access may fail to authenticate unless 'io-impl' is set " + + "explicitly. Schemes with a native FileIO: s3/s3a/s3n, gs, abfs/abfss/wasb/wasbs.", + warehouse, + gravitinoProperties.get(CredentialConstants.CREDENTIAL_PROVIDERS)); + } + + private void copyIfPresent(Map<String, String> source, String key, Map<String, String> target) { + String value = source.get(key); + if (StringUtils.isNotBlank(value)) { + target.put(key, value); + } + } + @Override public Map<String, String> toGravitinoTableProperties(Map<String, String> properties) { return new HashMap<>(properties); diff --git a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/plugin/GravitinoDriverPlugin.java b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/plugin/GravitinoDriverPlugin.java index ea0e03aa9d..59636ec0ef 100644 --- a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/plugin/GravitinoDriverPlugin.java +++ b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/plugin/GravitinoDriverPlugin.java @@ -112,6 +112,7 @@ public class GravitinoDriverPlugin implements DriverPlugin { this.catalogManager = GravitinoCatalogManager.create( + metalake, () -> createGravitinoClient( gravitinoUri, metalake, conf, sc.sparkUser(), gravitinoClientConfig)); diff --git a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/glue/TestGravitinoGlueCatalog.java b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/glue/TestGravitinoGlueCatalog.java index af980f7309..c30265f089 100644 --- a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/glue/TestGravitinoGlueCatalog.java +++ b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/glue/TestGravitinoGlueCatalog.java @@ -66,7 +66,7 @@ public class TestGravitinoGlueCatalog { // GravitinoGlueCatalog extends BaseCatalog which calls GravitinoCatalogManager.get() // in its constructor, so we must initialize the manager first. GravitinoClient mockClient = mock(GravitinoClient.class); - GravitinoCatalogManager.create(() -> mockClient); + GravitinoCatalogManager.create("test_metalake", () -> mockClient); } @AfterAll diff --git a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/iceberg/TestIcebergPropertiesConverter.java b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/iceberg/TestIcebergPropertiesConverter.java index 643a977cdb..e59fe216da 100644 --- a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/iceberg/TestIcebergPropertiesConverter.java +++ b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/iceberg/TestIcebergPropertiesConverter.java @@ -140,4 +140,140 @@ public class TestIcebergPropertiesConverter { "custom-warehouse"), properties); } + + @Test + void testIcebergRestPropertiesFromHiveBackendExcludesStaticCredentials() { + Map<String, String> gravitinoProperties = + ImmutableMap.of( + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND, + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND_HIVE, + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_URI, + "thrift://hive-metastore:9083", + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_WAREHOUSE, + "s3://bucket/warehouse", + "s3-access-key-id", + "AKIDEXAMPLE", + "s3-secret-access-key", + "secret"); + + Map<String, String> properties = + icebergPropertiesConverter.buildIcebergRestProperties( + "my_catalog", "http://gravitino:9001/iceberg", gravitinoProperties, ImmutableMap.of()); + + Assertions.assertEquals( + "rest", properties.get(IcebergPropertiesConstants.ICEBERG_CATALOG_TYPE)); + Assertions.assertEquals( + "http://gravitino:9001/iceberg", + properties.get(IcebergPropertiesConstants.ICEBERG_CATALOG_URI)); + Assertions.assertEquals( + "my_catalog", properties.get(IcebergPropertiesConstants.ICEBERG_CATALOG_WAREHOUSE)); + Assertions.assertEquals("my_catalog", properties.get("prefix")); + Assertions.assertEquals( + "vended-credentials", properties.get("header.X-Iceberg-Access-Delegation")); + Assertions.assertEquals( + "org.apache.iceberg.aws.s3.S3FileIO", properties.get(IcebergConstants.IO_IMPL)); + + // The REST protocol vends its own credentials, so no static secret should be present. + Assertions.assertFalse(properties.containsKey(IcebergConstants.ICEBERG_S3_ACCESS_KEY_ID)); + Assertions.assertFalse(properties.containsKey(IcebergConstants.ICEBERG_S3_SECRET_ACCESS_KEY)); + Assertions.assertFalse(properties.containsKey(IcebergConstants.ICEBERG_JDBC_USER)); + Assertions.assertFalse(properties.containsKey(IcebergConstants.ICEBERG_JDBC_PASSWORD)); + } + + @Test + void testIcebergRestPropertiesFromJdbcBackendExcludesStaticCredentials() { + Map<String, String> gravitinoProperties = + ImmutableMap.of( + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND, + IcebergPropertiesConstants.ICEBERG_CATALOG_BACKEND_JDBC, + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_URI, + "jdbc:postgresql://db:5432/iceberg", + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_JDBC_USER, + "user", + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_JDBC_PASSWORD, + "passwd"); + + Map<String, String> properties = + icebergPropertiesConverter.buildIcebergRestProperties( + "my_catalog", "http://gravitino:9001/iceberg", gravitinoProperties, ImmutableMap.of()); + + Assertions.assertEquals( + "rest", properties.get(IcebergPropertiesConstants.ICEBERG_CATALOG_TYPE)); + Assertions.assertFalse(properties.containsKey(IcebergConstants.ICEBERG_JDBC_USER)); + Assertions.assertFalse(properties.containsKey(IcebergConstants.ICEBERG_JDBC_PASSWORD)); + } + + @Test + void testIcebergRestPropertiesRespectsExplicitIoImpl() { + Map<String, String> gravitinoProperties = + ImmutableMap.of( + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND, + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND_HIVE, + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_WAREHOUSE, + "s3://bucket/warehouse", + IcebergConstants.IO_IMPL, + "com.example.CustomFileIO"); + + Map<String, String> properties = + icebergPropertiesConverter.buildIcebergRestProperties( + "my_catalog", "http://gravitino:9001/iceberg", gravitinoProperties, ImmutableMap.of()); + + Assertions.assertEquals("com.example.CustomFileIO", properties.get(IcebergConstants.IO_IMPL)); + } + + @Test + void testIcebergRestPropertiesWarehouseSchemeWithoutNativeFileIo() { + Map<String, String> gravitinoProperties = + ImmutableMap.of( + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND, + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND_HIVE, + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_WAREHOUSE, + "hdfs://namenode/warehouse"); + + Map<String, String> properties = + icebergPropertiesConverter.buildIcebergRestProperties( + "my_catalog", "http://gravitino:9001/iceberg", gravitinoProperties, ImmutableMap.of()); + + Assertions.assertFalse(properties.containsKey(IcebergConstants.IO_IMPL)); + } + + @Test + void testIcebergRestPropertiesBypassAndReservedKeyOverride() { + Map<String, String> gravitinoProperties = + ImmutableMap.of( + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND, + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND_HIVE, + "spark.bypass.some-custom-property", + "custom-value", + // Attempting to override a reserved routing key via spark.bypass must be ignored. + "spark.bypass." + IcebergPropertiesConstants.ICEBERG_CATALOG_URI, + "http://attacker-controlled/iceberg"); + + Map<String, String> properties = + icebergPropertiesConverter.buildIcebergRestProperties( + "my_catalog", "http://gravitino:9001/iceberg", gravitinoProperties, ImmutableMap.of()); + + Assertions.assertEquals("custom-value", properties.get("some-custom-property")); + Assertions.assertEquals( + "http://gravitino:9001/iceberg", + properties.get(IcebergPropertiesConstants.ICEBERG_CATALOG_URI)); + } + + @Test + void testIcebergRestPropertiesRestClientConfigPassthrough() { + Map<String, String> gravitinoProperties = + ImmutableMap.of( + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND, + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND_HIVE); + + Map<String, String> properties = + icebergPropertiesConverter.buildIcebergRestProperties( + "my_catalog", + "http://gravitino:9001/iceberg", + gravitinoProperties, + ImmutableMap.of("rest.auth.type", "basic", "rest.auth.basic.username", "admin")); + + Assertions.assertEquals("basic", properties.get("rest.auth.type")); + Assertions.assertEquals("admin", properties.get("rest.auth.basic.username")); + } } diff --git a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/integration/test/SparkEnvIT.java b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/integration/test/SparkEnvIT.java index fcd2c2d819..8344ccc21b 100644 --- a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/integration/test/SparkEnvIT.java +++ b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/integration/test/SparkEnvIT.java @@ -28,6 +28,7 @@ import java.util.HashMap; import java.util.Map; import org.apache.gravitino.Catalog; import org.apache.gravitino.NameIdentifier; +import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergConstants; import org.apache.gravitino.client.GravitinoMetalake; import org.apache.gravitino.function.FunctionDefinition; import org.apache.gravitino.function.FunctionDefinitions; @@ -84,6 +85,16 @@ public abstract class SparkEnvIT extends SparkUtilIT { return true; } + /** + * Whether the Iceberg REST auxiliary service should run with {@code dynamic-config-provider} + * (routing requests to whichever Gravitino catalog the request's {@code prefix} names) instead of + * the default single-catalog {@code static-config-provider}. Tests that verify the Spark + * connector's Iceberg REST auto-discovery/routing need this. + */ + protected boolean useDynamicIcebergRestConfigProvider() { + return false; + } + /** Returns the Gravitino {@link Catalog} for the catalog under test. */ protected Catalog getGravitinoCatalog() { return client.loadMetalake(metalakeName).loadCatalog(getCatalogName()); @@ -232,24 +243,36 @@ public abstract class SparkEnvIT extends SparkUtilIT { private void initIcebergRestServiceEnv() { super.ignoreIcebergAuxRestService = false; Map<String, String> icebergRestServiceConfigs = new HashMap<>(); - icebergRestServiceConfigs.put( - "gravitino." - + icebergRestServiceName - + "." - + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND, - IcebergPropertiesConstants.ICEBERG_CATALOG_BACKEND_HIVE); - icebergRestServiceConfigs.put( - "gravitino." - + icebergRestServiceName - + "." - + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_URI, - hiveMetastoreUri); - icebergRestServiceConfigs.put( - "gravitino." - + icebergRestServiceName - + "." - + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_WAREHOUSE, - warehouse); + if (useDynamicIcebergRestConfigProvider()) { + icebergRestServiceConfigs.put( + "gravitino." + + icebergRestServiceName + + "." + + IcebergConstants.ICEBERG_REST_CATALOG_CONFIG_PROVIDER, + IcebergConstants.DYNAMIC_ICEBERG_CATALOG_CONFIG_PROVIDER_NAME); + icebergRestServiceConfigs.put( + "gravitino." + icebergRestServiceName + "." + IcebergConstants.GRAVITINO_METALAKE, + metalakeName); + } else { + icebergRestServiceConfigs.put( + "gravitino." + + icebergRestServiceName + + "." + + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND, + IcebergPropertiesConstants.ICEBERG_CATALOG_BACKEND_HIVE); + icebergRestServiceConfigs.put( + "gravitino." + + icebergRestServiceName + + "." + + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_URI, + hiveMetastoreUri); + icebergRestServiceConfigs.put( + "gravitino." + + icebergRestServiceName + + "." + + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_WAREHOUSE, + warehouse); + } registerCustomConfigs(icebergRestServiceConfigs); } diff --git a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/integration/test/iceberg/SparkIcebergCatalogRestRoutingIT.java b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/integration/test/iceberg/SparkIcebergCatalogRestRoutingIT.java new file mode 100644 index 0000000000..80b726429b --- /dev/null +++ b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/integration/test/iceberg/SparkIcebergCatalogRestRoutingIT.java @@ -0,0 +1,76 @@ +/* + * 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.spark.connector.integration.test.iceberg; + +import com.google.common.collect.Maps; +import java.util.Map; +import org.apache.gravitino.spark.connector.iceberg.GravitinoIcebergCatalog; +import org.apache.gravitino.spark.connector.iceberg.IcebergPropertiesConstants; +import org.apache.iceberg.catalog.Catalog; +import org.apache.spark.sql.connector.catalog.CatalogPlugin; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIf; + +/** + * This class configures the Gravitino Iceberg catalog with a Hive backend, but runs the Iceberg + * REST auxiliary service in {@code dynamic-config-provider} mode so the Spark connector + * auto-discovers it and routes the catalog through the Iceberg REST protocol instead of talking to + * the Hive metastore directly. All test cases inherited from {@link SparkIcebergCatalogIT} exercise + * this REST-routed path end to end. + */ +@Tag("gravitino-docker-test") +// Spark connector uses a low Iceberg version, couldn't work with Iceberg REST server with high +// Iceberg version in embedded mode. +@DisabledIf("org.apache.gravitino.integration.test.util.ITUtils#isEmbedded") +public abstract class SparkIcebergCatalogRestRoutingIT extends SparkIcebergCatalogIT { + + @Override + protected boolean useDynamicIcebergRestConfigProvider() { + return true; + } + + @Override + protected Map<String, String> getCatalogConfigs() { + Map<String, String> catalogProperties = Maps.newHashMap(); + catalogProperties.put( + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_BACKEND, + IcebergPropertiesConstants.ICEBERG_CATALOG_BACKEND_HIVE); + catalogProperties.put( + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_WAREHOUSE, warehouse); + catalogProperties.put( + IcebergPropertiesConstants.GRAVITINO_ICEBERG_CATALOG_URI, hiveMetastoreUri); + return catalogProperties; + } + + @Test + void testCatalogIsRoutedThroughIcebergRestServer() { + CatalogPlugin catalogPlugin = + getSparkSession().sessionState().catalogManager().catalog(getCatalogName()); + Assertions.assertInstanceOf(GravitinoIcebergCatalog.class, catalogPlugin); + + Catalog icebergCatalog = ((GravitinoIcebergCatalog) catalogPlugin).icebergCatalog(); + Assertions.assertEquals( + "org.apache.iceberg.rest.RESTCatalog", + icebergCatalog.getClass().getName(), + "The hive-backed catalog should have been routed through the discovered Iceberg REST " + + "server, instead of Iceberg's native HiveCatalog."); + } +} diff --git a/spark-connector/v3.3/spark/src/test/java/org/apache/gravitino/spark/connector/integration/test/iceberg/SparkIcebergCatalogRestRoutingIT33.java b/spark-connector/v3.3/spark/src/test/java/org/apache/gravitino/spark/connector/integration/test/iceberg/SparkIcebergCatalogRestRoutingIT33.java new file mode 100644 index 0000000000..181cf6a0bc --- /dev/null +++ b/spark-connector/v3.3/spark/src/test/java/org/apache/gravitino/spark/connector/integration/test/iceberg/SparkIcebergCatalogRestRoutingIT33.java @@ -0,0 +1,62 @@ +/* + * 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.spark.connector.integration.test.iceberg; + +import org.apache.gravitino.spark.connector.integration.test.util.SparkMetadataColumnInfo; +import org.apache.spark.sql.types.DataTypes; +import org.apache.spark.sql.types.StructField; +import org.junit.jupiter.api.condition.DisabledIf; + +/** + * Spark 3.3 integration tests for Iceberg REST routing. + * + * <p>Spark 3.3 pins Iceberg to 1.8.x ({@code iceberg4spark33}); Gravitino's lakehouse Iceberg stack + * is 1.11. {@link DisabledIf} skips embedded mode to avoid classpath conflicts. Deploy-mode tests + * run a 1.8.x client against Gravitino's 1.11 REST server and cover basic catalog CRUD only; they + * do not exercise Iceberg 1.11 REST changes such as {@code file-scan-tasks} scan planning or the + * configurable multi-level namespace separator contract. + */ +@DisabledIf("org.apache.gravitino.integration.test.util.ITUtils#isEmbedded") +public class SparkIcebergCatalogRestRoutingIT33 extends SparkIcebergCatalogRestRoutingIT { + @Override + protected boolean supportsFunction() { + // Spark 3.3 does not support function operations + return false; + } + + /** + * Spark 3.3 uses Iceberg 1.8.x; its {@code SparkTable#metadataColumns()} exposes five columns, + * not the seven row-lineage columns added in Iceberg 1.11. + */ + @Override + protected SparkMetadataColumnInfo[] getIcebergMetadataColumns() { + return new SparkMetadataColumnInfo[] { + new SparkMetadataColumnInfo("_spec_id", DataTypes.IntegerType, false), + new SparkMetadataColumnInfo( + "_partition", + DataTypes.createStructType( + new StructField[] {DataTypes.createStructField("name", DataTypes.StringType, true)}), + true), + new SparkMetadataColumnInfo("_file", DataTypes.StringType, false), + new SparkMetadataColumnInfo("_pos", DataTypes.LongType, false), + new SparkMetadataColumnInfo("_deleted", DataTypes.BooleanType, false) + }; + } +} diff --git a/spark-connector/v3.4/spark/src/test/java/org/apache/gravitino/spark/connector/integration/test/iceberg/SparkIcebergCatalogRestRoutingIT34.java b/spark-connector/v3.4/spark/src/test/java/org/apache/gravitino/spark/connector/integration/test/iceberg/SparkIcebergCatalogRestRoutingIT34.java new file mode 100644 index 0000000000..ae23429b09 --- /dev/null +++ b/spark-connector/v3.4/spark/src/test/java/org/apache/gravitino/spark/connector/integration/test/iceberg/SparkIcebergCatalogRestRoutingIT34.java @@ -0,0 +1,25 @@ +/* + * 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.spark.connector.integration.test.iceberg; + +import org.junit.jupiter.api.condition.DisabledIf; + +@DisabledIf("org.apache.gravitino.integration.test.util.ITUtils#isEmbedded") +public class SparkIcebergCatalogRestRoutingIT34 extends SparkIcebergCatalogRestRoutingIT {} diff --git a/spark-connector/v3.5/spark/src/test/java/org/apache/gravitino/spark/connector/integration/test/iceberg/SparkIcebergCatalogRestRoutingIT35.java b/spark-connector/v3.5/spark/src/test/java/org/apache/gravitino/spark/connector/integration/test/iceberg/SparkIcebergCatalogRestRoutingIT35.java new file mode 100644 index 0000000000..7a091bc0c5 --- /dev/null +++ b/spark-connector/v3.5/spark/src/test/java/org/apache/gravitino/spark/connector/integration/test/iceberg/SparkIcebergCatalogRestRoutingIT35.java @@ -0,0 +1,25 @@ +/* + * 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.spark.connector.integration.test.iceberg; + +import org.junit.jupiter.api.condition.DisabledIf; + +@DisabledIf("org.apache.gravitino.integration.test.util.ITUtils#isEmbedded") +public class SparkIcebergCatalogRestRoutingIT35 extends SparkIcebergCatalogRestRoutingIT {}
