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 320489684ee048076101c49510e285b500e51013 Author: diqiu50 <[email protected]> AuthorDate: Mon Aug 24 15:26:50 2026 +0800 [Cherry-pick to branch-1.3] [#12554] improvement(trino-connector): Discover the Iceberg REST server instead of a manual switch Replace gravitino.iceberg.rest-enabled with automatic discovery. The Trino connector already connects to the Gravitino server to load metalakes, so it now also asks that server whether it has an Iceberg REST server running for the connector's metalake, and routes lakehouse-iceberg catalogs through the reported endpoint. When the server reports nothing — the Iceberg REST server is not running, or serves a different metalake — catalogs fall back to translating catalog-backend as before, with no configuration required either way. Server side: AuxiliaryServiceManager gains a query method for whether an auxiliary service is registered, and a new GET /api/system/iceberg-rest endpoint reports the Iceberg REST server's address for a given metalake, falling back to the request's own host when the service binds to a wildcard address. The client gains a matching method following the existing REST client patterns. gravitino.iceberg.rest-uri remains as a manual override for topologies where the discovered address is not reachable. The discovery call is piggybacked on the existing periodic metalake poll, so it adds no new network schedule, and failures (including talking to a Gravitino server older than this endpoint) are swallowed rather than interrupting catalog loading. This also resolves the multi-metalake ambiguity in the previous design: the server only reports an endpoint for the metalake it is configured to serve, so a same-named catalog in a different metalake can no longer be silently misrouted. (cherry picked from commit a8015c9e3fc860bf0059adce70244646ea869492) --- .../gravitino/client/GravitinoClientBase.java | 23 ++++ .../dto/responses/IcebergRESTServiceResponse.java | 56 ++++++++ .../java/org/apache/gravitino/GravitinoEnv.java | 9 ++ .../auxiliary/AuxiliaryServiceManager.java | 10 ++ docs/trino-connector/authentication.md | 3 +- docs/trino-connector/catalog-iceberg.md | 28 ++-- docs/trino-connector/configuration.md | 13 +- .../docker-script/docker-compose.yaml | 2 - .../init/trino/config/catalog/gravitino.properties | 1 - .../docker-script/init/trino/init.sh | 4 - .../integration/test/container/ContainerSuite.java | 23 +--- .../test/container/TrinoITContainers.java | 10 +- .../gravitino/integration/test/util/BaseIT.java | 11 -- .../web/rest/IcebergRESTServiceOperations.java | 105 +++++++++++++++ .../integration/test/TrinoConnectorIT.java | 1 - .../integration/test/TrinoQueryITBase.java | 1 - .../gravitino/trino/connector/GravitinoConfig.java | 84 +++++------- .../connector/catalog/CatalogConnectorManager.java | 18 +++ .../iceberg/IcebergCatalogPropertyConverter.java | 28 ++-- .../catalog/iceberg/IcebergConnectorAdapter.java | 11 +- .../trino/connector/TestGravitinoConfig.java | 55 +++++--- .../TestIcebergCatalogPropertyConverter.java | 148 ++++++++++++++++----- 22 files changed, 453 insertions(+), 191 deletions(-) diff --git a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClientBase.java b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClientBase.java index 996c149591..07c7e9cca6 100644 --- a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClientBase.java +++ b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClientBase.java @@ -29,10 +29,12 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Collections; import java.util.Map; +import java.util.Optional; import org.apache.commons.lang3.StringUtils; import org.apache.gravitino.NameIdentifier; import org.apache.gravitino.Namespace; import org.apache.gravitino.Version; +import org.apache.gravitino.dto.responses.IcebergRESTServiceResponse; import org.apache.gravitino.dto.responses.MetalakeResponse; import org.apache.gravitino.dto.responses.VersionResponse; import org.apache.gravitino.exceptions.GravitinoRuntimeException; @@ -185,6 +187,27 @@ public abstract class GravitinoClientBase implements Closeable { return new GravitinoVersion(resp.getVersion()); } + /** + * Retrieves the endpoint of the Gravitino Iceberg REST server, if it is running and serves the + * given metalake. + * + * @param metalakeName the metalake the caller intends to route through the Iceberg REST server + * @return the Iceberg REST server endpoint, or empty if it is not running or serves a different + * metalake + */ + public Optional<String> icebergRestServiceUri(String metalakeName) { + IcebergRESTServiceResponse resp = + restClient.get( + "api/system/iceberg-rest", + ImmutableMap.of("metalake", metalakeName), + IcebergRESTServiceResponse.class, + Collections.emptyMap(), + ErrorHandlers.restErrorHandler()); + resp.validate(); + + return Optional.ofNullable(resp.getUri()); + } + /** Closes the GravitinoClient and releases any underlying resources. */ @Override public void close() { diff --git a/common/src/main/java/org/apache/gravitino/dto/responses/IcebergRESTServiceResponse.java b/common/src/main/java/org/apache/gravitino/dto/responses/IcebergRESTServiceResponse.java new file mode 100644 index 0000000000..89c071dfcc --- /dev/null +++ b/common/src/main/java/org/apache/gravitino/dto/responses/IcebergRESTServiceResponse.java @@ -0,0 +1,56 @@ +/* + * 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.dto.responses; + +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.Nullable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * Represents a response reporting the endpoint of the Gravitino Iceberg REST server, if it is + * running and serves the requested metalake. + */ +@Getter +@EqualsAndHashCode(callSuper = true) +@ToString +public class IcebergRESTServiceResponse extends BaseResponse { + + @Nullable + @JsonProperty("uri") + private final String uri; + + /** + * Constructor for IcebergRESTServiceResponse. + * + * @param uri the Iceberg REST server endpoint, or {@code null} when it is not running or does not + * serve the requested metalake + */ + public IcebergRESTServiceResponse(@Nullable String uri) { + super(0); + this.uri = uri; + } + + /** Default constructor for IcebergRESTServiceResponse. (Used for Jackson deserialization.) */ + public IcebergRESTServiceResponse() { + super(); + this.uri = null; + } +} diff --git a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java index 4f42989045..fc69a7cbde 100644 --- a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java +++ b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java @@ -235,6 +235,15 @@ public class GravitinoEnv { return config; } + /** + * Get the auxiliary service manager associated with the Gravitino environment. + * + * @return The auxiliary service manager instance. + */ + public AuxiliaryServiceManager auxServiceManager() { + return auxServiceManager; + } + /** * Get the EntityStore associated with the Gravitino environment. * diff --git a/core/src/main/java/org/apache/gravitino/auxiliary/AuxiliaryServiceManager.java b/core/src/main/java/org/apache/gravitino/auxiliary/AuxiliaryServiceManager.java index 08444abb3f..accb23f799 100644 --- a/core/src/main/java/org/apache/gravitino/auxiliary/AuxiliaryServiceManager.java +++ b/core/src/main/java/org/apache/gravitino/auxiliary/AuxiliaryServiceManager.java @@ -95,6 +95,16 @@ public class AuxiliaryServiceManager { } @VisibleForTesting + /** + * Returns whether the given auxiliary service was configured and registered. + * + * @param auxServiceName the auxiliary service's short name, e.g. {@code iceberg-rest} + * @return true if the service is registered + */ + public boolean isAuxServiceRegistered(String auxServiceName) { + return auxServices.containsKey(auxServiceName); + } + public IsolatedClassLoader getIsolatedClassLoader(List<String> classPaths) { return IsolatedClassLoader.buildClassLoader(classPaths); } diff --git a/docs/trino-connector/authentication.md b/docs/trino-connector/authentication.md index 322eef9f5d..42c7932f6b 100644 --- a/docs/trino-connector/authentication.md +++ b/docs/trino-connector/authentication.md @@ -194,13 +194,12 @@ Whether the coordinator can populate this extra-credential depends on the Trino The `gravitino.client.oauth2.*` properties above still configure the shared bootstrap/admin client used for catalog discovery — they are unrelated to the per-user forwarded token. For an Iceberg catalog reached through the Gravitino Iceberg REST server (IRC) — every -`lakehouse-iceberg` catalog, unless `gravitino.iceberg.rest-enabled=false`; see [Iceberg +`lakehouse-iceberg` catalog for which the Gravitino server reports a running IRC; see [Iceberg catalog](./catalog-iceberg.md#how-trino-reaches-the-catalog) — the IRC's own authentication is configured once per Trino cluster with the `gravitino.iceberg.rest-catalog.` prefix, and `iceberg.rest-catalog.session=USER` is set automatically when `forwardUser=true`: ```properties -gravitino.iceberg.rest-uri=http://gravitino-host:9001/iceberg gravitino.iceberg.rest-catalog.security=OAUTH2 gravitino.iceberg.rest-catalog.oauth2.credential=service-account-id:service-account-secret gravitino.iceberg.rest-catalog.oauth2.server-uri=http://your-idp/realms/gravitino/protocol/openid-connect/token diff --git a/docs/trino-connector/catalog-iceberg.md b/docs/trino-connector/catalog-iceberg.md index dd00d8cab1..02779e8041 100644 --- a/docs/trino-connector/catalog-iceberg.md +++ b/docs/trino-connector/catalog-iceberg.md @@ -33,7 +33,17 @@ nowhere to put the session token of an STS temporary credential — so a catalog IRC means every table access gets a freshly issued temporary credential over the Iceberg REST protocol. -Configure the IRC endpoint once per Trino cluster, in `etc/catalog/gravitino.properties`: +The connector already connects to the Gravitino server (it is how catalogs are discovered in the +first place), so it also asks that server whether it has an Iceberg REST server running for the +connector's metalake, and uses the endpoint the server reports. Nothing needs to be configured for +this: `etc/catalog/gravitino.properties` only needs the usual `gravitino.uri` and +`gravitino.metalake`. If the server reports no endpoint — the IRC is not running, or it serves a +different metalake — `lakehouse-iceberg` catalogs fall back to translating `catalog-backend` as +before, and credential vending does not work. + +Set `gravitino.iceberg.rest-uri` only to override the discovered endpoint, for example when the IRC +is not reachable at the address the Gravitino server itself reports (a split network, or the IRC +deployed independently of the main server): ```properties connector.name=gravitino @@ -43,10 +53,6 @@ gravitino.uri=http://gravitino-host:8090 gravitino.iceberg.rest-uri=http://gravitino-host:9001/iceberg ``` -The endpoint is deployment topology, not a property of the data source, so it does not belong on the -catalog: it would have to be repeated on every new catalog, moving the IRC would mean editing all of -them, and one catalog could not serve two Trino clusters that reach the IRC by different hostnames. - The connector derives everything else from the catalog itself. The Gravitino catalog name is passed as both `iceberg.rest-catalog.warehouse` and `iceberg.rest-catalog.prefix` — the Iceberg client selects the catalog twice over, first as the query parameter of the `GET /v1/config` call that @@ -106,14 +112,14 @@ keeping per-user credential vending and per-user authorization intact. Set ### Limitations - One IRC serves exactly one metalake, fixed at startup by - `gravitino.iceberg-rest.gravitino-metalake`. In multi-metalake mode - (`gravitino.use-single-metalake=false`), only catalogs in that metalake are reachable; the others - fail on the IRC side with `NoSuchCatalogException`. + `gravitino.iceberg-rest.gravitino-metalake`. The Gravitino server only reports the IRC's endpoint + for that metalake; in multi-metalake mode (`gravitino.use-single-metalake=false`), catalogs in any + other metalake fall back to translating `catalog-backend` instead of failing outright. - A catalog created with `catalog-backend=rest` keeps pointing at its own configured `uri` and is not re-routed, since it already reaches an Iceberg REST catalog directly. -- To disable this routing entirely — for a deployment that does not run the IRC — set - `gravitino.iceberg.rest-enabled=false`. Iceberg catalogs are then translated into Trino's `jdbc` - or `hive_metastore` catalog types as before, and credential vending does not work. +- A deployment that does not run the IRC needs no configuration at all: the server reports no + endpoint, and `lakehouse-iceberg` catalogs are translated into Trino's `jdbc` or `hive_metastore` + catalog types as before. ## Schema Operations diff --git a/docs/trino-connector/configuration.md b/docs/trino-connector/configuration.md index 6d225a6cec..9e6747ef67 100644 --- a/docs/trino-connector/configuration.md +++ b/docs/trino-connector/configuration.md @@ -29,21 +29,16 @@ license: "This software is licensed under the Apache License version 2." | gravitino.client. | string | (none) | The configuration key prefix for the Gravitino client config. | No | | gravitino.trino.skip-catalog-patterns | string | (none) | The `gravitino.trino.skip-catalog-patterns` defines a comma-separated list of catalog name regex patterns that should be excluded from loading. For example, `test_.*, .*_tmp` excludes all catalogs starting with `test_` or ending with `_tmp`. | No | | gravitino.use-single-metalake | boolean | true | If `true`, only one metalake is used and catalogs are identified by `<catalog_name>`. If `false`, multi-metalake mode is enabled and catalogs are identified by `<metalake_name>.<catalog_name>`. | No | -| gravitino.iceberg.rest-enabled | boolean | true | If `true`, `lakehouse-iceberg` catalogs are loaded through the Gravitino Iceberg REST server (IRC) instead of being translated into Trino's `jdbc` or `hive_metastore` Iceberg catalog type. This is what makes credential vending work. Requires `gravitino.iceberg.rest-uri`. Set to `false` for a deployment that does not run the IRC. | No | -| gravitino.iceberg.rest-uri | string | (none) | The endpoint of the Gravitino Iceberg REST server, for example `http://gravitino-host:9001/iceberg`. Required when `gravitino.iceberg.rest-enabled` is `true` and the metalake contains `lakehouse-iceberg` catalogs whose `catalog-backend` is not `rest`. | No | +| gravitino.iceberg.rest-uri | string | (none) | The endpoint of the Gravitino Iceberg REST server (IRC). Discovered automatically from the Gravitino server, which is asked whether it has an IRC running for this connector's metalake; set this only to override the discovered value. When an endpoint is available (discovered or configured), `lakehouse-iceberg` catalogs whose `catalog-backend` is not `rest` are loaded through it instead of being translated in [...] | gravitino.iceberg.rest-catalog. | string | (none) | The configuration key prefix for properties passed through to the internal Trino Iceberg REST catalog. The prefix is rewritten to `iceberg.rest-catalog.`, so `gravitino.iceberg.rest-catalog.security=OAUTH2` becomes `iceberg.rest-catalog.security=OAUTH2`. The `uri`, `warehouse` and `prefix` keys are reserved and always derived by the connector. | No | To configure the Gravitino client, use properties prefixed with `gravitino.client.`. These properties will directly passed to the Gravitino client. **Note:** Invalid configuration properties will result in exceptions. Please see [Gravitino Java client configurations](../how-to-use-gravitino-client.md#java-client-configuration) for more support client configuration. -:::caution -`gravitino.iceberg.rest-enabled` defaults to `true`. When upgrading an existing deployment whose -metalake contains `lakehouse-iceberg` catalogs with `catalog-backend=jdbc` or `hive`, those catalogs -fail to load unless you either set `gravitino.iceberg.rest-uri` to your Iceberg REST server endpoint, -or set `gravitino.iceberg.rest-enabled=false` to keep the previous behavior. See -[Iceberg catalog](./catalog-iceberg.md#how-trino-reaches-the-catalog). -::: +Upgrading an existing deployment needs no action: a Gravitino server without the Iceberg REST server +running reports no endpoint, so `lakehouse-iceberg` catalogs keep translating `catalog-backend` as +before. See [Iceberg catalog](./catalog-iceberg.md#how-trino-reaches-the-catalog). Multi-metalake mode (`gravitino.use-single-metalake=false`) is supported on Trino connector versions 440-445 and 469-478. On versions 446-468, a warning is logged and the connector initializes, but the mode is not fully supported and some operations may fail. diff --git a/integration-test-common/docker-script/docker-compose.yaml b/integration-test-common/docker-script/docker-compose.yaml index 1dfb8f5a7d..3ed57d07ca 100644 --- a/integration-test-common/docker-script/docker-compose.yaml +++ b/integration-test-common/docker-script/docker-compose.yaml @@ -87,7 +87,6 @@ services: - HADOOP_USER_NAME=anonymous - GRAVITINO_HOST_IP=host.docker.internal - GRAVITINO_HOST_PORT=${GRAVITINO_SERVER_PORT:-8090} - - GRAVITINO_ICEBERG_REST_PORT=${GRAVITINO_ICEBERG_REST_PORT:-9001} - GRAVITINO_METALAKE_NAME=test - HIVE_HOST_IP=hive - TRINO_WORKER_NUM=${TRINO_WORKER_NUM:-0} @@ -123,7 +122,6 @@ services: - HADOOP_USER_NAME=anonymous - GRAVITINO_HOST_IP=host.docker.internal - GRAVITINO_HOST_PORT=${GRAVITINO_SERVER_PORT:-8090} - - GRAVITINO_ICEBERG_REST_PORT=${GRAVITINO_ICEBERG_REST_PORT:-9001} - GRAVITINO_METALAKE_NAME=test - HIVE_HOST_IP=hive - TRINO_ROLE=worker diff --git a/integration-test-common/docker-script/init/trino/config/catalog/gravitino.properties b/integration-test-common/docker-script/init/trino/config/catalog/gravitino.properties index d38c18c458..08789c84c0 100644 --- a/integration-test-common/docker-script/init/trino/config/catalog/gravitino.properties +++ b/integration-test-common/docker-script/init/trino/config/catalog/gravitino.properties @@ -20,7 +20,6 @@ connector.name = gravitino gravitino.uri = http://GRAVITINO_HOST_IP:GRAVITINO_HOST_PORT gravitino.metalake = GRAVITINO_METALAKE_NAME -gravitino.iceberg.rest-uri = http://GRAVITINO_ICEBERG_REST_HOST:GRAVITINO_ICEBERG_REST_PORT/iceberg gravitino.trino.skip-version-validation=true gravitino.client.authType = simple gravitino.client.session.forwardUser = true diff --git a/integration-test-common/docker-script/init/trino/init.sh b/integration-test-common/docker-script/init/trino/init.sh index a65ec5ab90..077c6f77d0 100644 --- a/integration-test-common/docker-script/init/trino/init.sh +++ b/integration-test-common/docker-script/init/trino/init.sh @@ -34,10 +34,6 @@ cp /usr/lib/trino/plugin/mysql/*mysql-connector-j-*.jar /usr/lib/trino/plugin/ic sed -i "s/GRAVITINO_HOST_IP:GRAVITINO_HOST_PORT/${GRAVITINO_HOST_IP}:${GRAVITINO_HOST_PORT}/g" /etc/trino/catalog/gravitino.properties # Update `gravitino.metalake = GRAVITINO_METALAKE_NAME` in the `conf/catalog/gravitino.properties` sed -i "s/GRAVITINO_METALAKE_NAME/${GRAVITINO_METALAKE_NAME}/g" /etc/trino/catalog/gravitino.properties -# Update `gravitino.iceberg.rest-uri` in the `conf/catalog/gravitino.properties`. The Iceberg REST -# server runs on the same host as the Gravitino server but on a different port, so it needs its own -# placeholder pair — reusing GRAVITINO_HOST_IP:GRAVITINO_HOST_PORT would substitute the server port. -sed -i "s/GRAVITINO_ICEBERG_REST_HOST:GRAVITINO_ICEBERG_REST_PORT/${GRAVITINO_HOST_IP}:${GRAVITINO_ICEBERG_REST_PORT}/g" /etc/trino/catalog/gravitino.properties # Update `node.id=NODE_ID` in the `/conf/node.properties` sed -i "s/NODE_ID/${RANDOM}-${RANDOM}-${RANDOM}-${RANDOM}-${RANDOM}/g" /etc/trino/node.properties diff --git a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/ContainerSuite.java b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/ContainerSuite.java index ad35692dbb..b2b0ae50da 100644 --- a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/ContainerSuite.java +++ b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/ContainerSuite.java @@ -288,7 +288,6 @@ public class ContainerSuite implements Closeable { String trinoConfDir, String trinoConnectorLibDir, int gravitinoServerPort, - int icebergRestServerPort, String metalakeName) { ITUtils.cleanDisk(); if (trinoContainer == null) { @@ -297,23 +296,15 @@ public class ContainerSuite implements Closeable { initIfNecessary(); // Start Trino container String hiveContainerIp = hiveContainer.getContainerIpAddress(); - ImmutableMap.Builder<String, String> envVars = - ImmutableMap.<String, String>builder() - .put("HADOOP_USER_NAME", "anonymous") - .put("GRAVITINO_HOST_IP", "host.docker.internal") - .put("GRAVITINO_HOST_PORT", String.valueOf(gravitinoServerPort)) - .put("GRAVITINO_METALAKE_NAME", metalakeName); - if (icebergRestServerPort > 0) { - envVars.put("GRAVITINO_ICEBERG_REST_PORT", String.valueOf(icebergRestServerPort)); - } else { - LOG.info( - "No Iceberg REST server port supplied; Trino falls back to the compose default. " - + "Catalogs routed through the Iceberg REST server will not work in this " - + "container."); - } TrinoContainer.Builder trinoBuilder = TrinoContainer.builder() - .withEnvVars(envVars.build()) + .withEnvVars( + ImmutableMap.<String, String>builder() + .put("HADOOP_USER_NAME", "anonymous") + .put("GRAVITINO_HOST_IP", "host.docker.internal") + .put("GRAVITINO_HOST_PORT", String.valueOf(gravitinoServerPort)) + .put("GRAVITINO_METALAKE_NAME", metalakeName) + .build()) .withNetwork(getNetwork()) .withExtraHosts( ImmutableMap.<String, String>builder() diff --git a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/TrinoITContainers.java b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/TrinoITContainers.java index 572b948d55..a02e12de6a 100644 --- a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/TrinoITContainers.java +++ b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/TrinoITContainers.java @@ -50,12 +50,11 @@ public class TrinoITContainers implements AutoCloseable { } public void launch(int gravitinoServerPort) throws Exception { - launch(gravitinoServerPort, 0, "hive2", false, null, null, null); + launch(gravitinoServerPort, "hive2", false, null, null, null); } public void launch( int gravitinoServerPort, - int icebergRestServerPort, String hiveRuntimeVersion, boolean isTrinoConnectorTest, Integer trinoWorkerNum, @@ -82,13 +81,6 @@ public class TrinoITContainers implements AutoCloseable { } env.put("GRAVITINO_SERVER_PORT", String.valueOf(gravitinoServerPort)); env.put("HIVE_RUNTIME_VERSION", hiveRuntimeVersion); - if (icebergRestServerPort > 0) { - env.put("GRAVITINO_ICEBERG_REST_PORT", String.valueOf(icebergRestServerPort)); - } else { - LOG.info( - "No Iceberg REST server port supplied; Trino falls back to the compose default. " - + "Catalogs routed through the Iceberg REST server will not work in this container."); - } env.put("TRINO_CONNECTOR_TEST", String.valueOf(isTrinoConnectorTest)); if (System.getProperty("gravitino.log.path") != null) { env.put("GRAVITINO_LOG_PATH", System.getProperty("gravitino.log.path")); 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 0c247a22f1..ef45535de9 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 @@ -30,7 +30,6 @@ import com.google.common.collect.ImmutableMap; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; -import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -687,16 +686,6 @@ public class BaseIT { this.customConfigs.putAll(icebergRestConfigs); } - /** - * Returns the port the Iceberg REST auxiliary service listens on. Only valid once {@link - * #startIntegrationTest()} has run, since it reads the started server's config. - * - * @return the Iceberg REST service port - */ - public int getIcebergRestServicePort() { - return URI.create(getIcebergRestServiceUri()).getPort(); - } - protected String getIcebergRestServiceUri() { JettyServerConfig jettyServerConfig = JettyServerConfig.fromConfig(serverConfig, String.format("gravitino.iceberg-rest.")); diff --git a/server/src/main/java/org/apache/gravitino/server/web/rest/IcebergRESTServiceOperations.java b/server/src/main/java/org/apache/gravitino/server/web/rest/IcebergRESTServiceOperations.java new file mode 100644 index 0000000000..0ea698a6a4 --- /dev/null +++ b/server/src/main/java/org/apache/gravitino/server/web/rest/IcebergRESTServiceOperations.java @@ -0,0 +1,105 @@ +/* + * 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.server.web.rest; + +import com.codahale.metrics.annotation.ResponseMetered; +import com.codahale.metrics.annotation.Timed; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.apache.commons.lang3.StringUtils; +import org.apache.gravitino.Config; +import org.apache.gravitino.GravitinoEnv; +import org.apache.gravitino.dto.responses.IcebergRESTServiceResponse; +import org.apache.gravitino.metrics.MetricNames; +import org.apache.gravitino.server.web.JettyServerConfig; +import org.apache.gravitino.server.web.Utils; + +/** + * Reports the endpoint of the Gravitino Iceberg REST server, so that clients which already connect + * to this Gravitino server can discover it instead of requiring it to be configured separately. + */ +@Path("/system/iceberg-rest") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class IcebergRESTServiceOperations { + + // Matches gravitino.auxService.names / AuxiliaryServiceManager's registration key. + private static final String AUX_SERVICE_NAME = "iceberg-rest"; + private static final String CONFIG_PREFIX = "gravitino.iceberg-rest."; + // The post-strip key used by the Iceberg REST server itself; see + // IcebergConstants.GRAVITINO_METALAKE and DynamicIcebergConfigProvider. + private static final String SERVED_METALAKE_KEY = CONFIG_PREFIX + "gravitino-metalake"; + + @Context private HttpServletRequest httpRequest; + + /** + * Reports the Iceberg REST server's endpoint for the requested metalake. + * + * @param metalake the metalake the caller intends to route through the Iceberg REST server; may + * be blank, in which case the endpoint is reported regardless of which metalake it serves + * @return a response whose {@code uri} is {@code null} when the Iceberg REST server is not + * running, or does not serve the requested metalake + */ + @GET + @Timed(name = "iceberg-rest-service." + MetricNames.HTTP_PROCESS_DURATION, absolute = true) + @ResponseMetered(name = "iceberg-rest-service", absolute = true) + public Response getIcebergRestServiceUri(@QueryParam("metalake") String metalake) { + return Utils.ok(new IcebergRESTServiceResponse(resolveUri(metalake))); + } + + private String resolveUri(String metalake) { + if (!GravitinoEnv.getInstance().auxServiceManager().isAuxServiceRegistered(AUX_SERVICE_NAME)) { + return null; + } + + Config config = GravitinoEnv.getInstance().config(); + String servedMetalake = config.getRawString(SERVED_METALAKE_KEY, ""); + if (StringUtils.isNotBlank(metalake) + && StringUtils.isNotBlank(servedMetalake) + && !servedMetalake.equals(metalake)) { + // The Iceberg REST server serves exactly one metalake. Routing a different metalake's + // catalogs at it would 404 on every request, so report it as unavailable instead. + return null; + } + + JettyServerConfig icebergRestConfig = JettyServerConfig.fromConfig(config, CONFIG_PREFIX); + String host = icebergRestConfig.getHost(); + if (isWildcardHost(host)) { + // The Iceberg REST server binds to all interfaces, so it has no single externally + // reachable address of its own. The caller already reached this Gravitino server at some + // resolvable host, so reuse it — this holds whenever both services share a host, which is + // the common case, and callers with a genuinely split topology can still set + // gravitino.iceberg.rest-uri manually. + host = httpRequest.getServerName(); + } + String scheme = icebergRestConfig.isEnableHttps() ? "https" : "http"; + return String.format("%s://%s:%d/iceberg", scheme, host, icebergRestConfig.getHttpPort()); + } + + private static boolean isWildcardHost(String host) { + return StringUtils.isBlank(host) || "0.0.0.0".equals(host) || "::".equals(host); + } +} diff --git a/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoConnectorIT.java b/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoConnectorIT.java index 1be5ef43bc..968e26fb30 100644 --- a/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoConnectorIT.java +++ b/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoConnectorIT.java @@ -131,7 +131,6 @@ public class TrinoConnectorIT extends BaseIT { trinoConfDir, System.getenv("GRAVITINO_ROOT_DIR") + "/trino-connector/build/libs", getGravitinoServerPort(), - getIcebergRestServicePort(), metalakeName); Assertions.assertTrue( containerSuite.getTrinoContainer().checkSyncCatalogFromGravitino(5, catalogName), diff --git a/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoQueryITBase.java b/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoQueryITBase.java index 24d33cbf2e..e5be0c11c4 100644 --- a/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoQueryITBase.java +++ b/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoQueryITBase.java @@ -120,7 +120,6 @@ public class TrinoQueryITBase { trinoITContainers.launch( baseIT.getGravitinoServerPort(), - baseIT.getIcebergRestServicePort(), hiveRuntimeVersion, isTrinoConnectorTest, trinoWorkerNum, diff --git a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConfig.java b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConfig.java index 50af268066..2c1f795afb 100644 --- a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConfig.java +++ b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConfig.java @@ -31,18 +31,15 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.apache.gravitino.trino.connector.security.GravitinoAuthProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** Gravitino config. */ public class GravitinoConfig { - private static final Logger LOG = LoggerFactory.getLogger(GravitinoConfig.class); - // Trino config keys /** The Trino discovery URI. */ private static final String TRINO_DISCOVERY_URI = "discovery.uri"; @@ -86,6 +83,9 @@ public class GravitinoConfig { private static final Map<String, ConfigEntry> CONFIG_DEFINITIONS = new HashMap<>(); private final Map<String, String> config; private final List<Pattern> skipCatalogPatternList; + // Iceberg REST server endpoints discovered from the Gravitino server, keyed by metalake. Written + // by the catalog connector manager's periodic poll and read on every Iceberg catalog load. + private final Map<String, String> discoveredIcebergRestUriByMetalake = new ConcurrentHashMap<>(); // Gravitino config entity private static final ConfigEntry GRAVITINO_URI = @@ -270,20 +270,13 @@ public class GravitinoConfig { "3600", false); - private static final ConfigEntry GRAVITINO_ICEBERG_REST_ENABLED = - new ConfigEntry( - "gravitino.iceberg.rest-enabled", - "When true, lakehouse-iceberg catalogs are loaded through the Gravitino Iceberg REST " - + "server instead of being translated into a Trino JDBC or Hive metastore Iceberg " - + "catalog. Requires gravitino.iceberg.rest-uri.", - "true", - false); - private static final ConfigEntry GRAVITINO_ICEBERG_REST_URI = new ConfigEntry( "gravitino.iceberg.rest-uri", - "The endpoint of the Gravitino Iceberg REST server, for example " - + "http://localhost:9001/iceberg.", + "The endpoint of the Gravitino Iceberg REST server. Discovered automatically from the " + + "Gravitino server by default; set this only to override the discovered value, " + + "for example when the Iceberg REST server is not reachable at the address the " + + "Gravitino server itself reports.", "", false); @@ -325,28 +318,6 @@ public class GravitinoConfig { "Config `gravitino.trino.skip-catalog-patterns` is invalid because it contains an illegal regular expression", e); } - warnOnMissingIcebergRestUri(); - } - - /** - * Warns at startup when the Iceberg REST routing is on but has no endpoint. Without this, the - * failure only surfaces once a lakehouse-iceberg catalog is loaded, and that error is swallowed - * by the catalog refresh loop, leaving the user with an unexplained missing catalog. - */ - private void warnOnMissingIcebergRestUri() { - // Only the statically configured connector warns; the dynamic per-catalog ones would repeat it. - if (isDynamicConnector() - || !isIcebergRestEnabled() - || StringUtils.isNotBlank(getIcebergRestUri())) { - return; - } - LOG.warn( - "'{}' is enabled but '{}' is not set, so every lakehouse-iceberg catalog will fail to " - + "load. Set the Iceberg REST server endpoint, or set '{}=false' to load Iceberg " - + "catalogs through their catalog backend instead.", - GRAVITINO_ICEBERG_REST_ENABLED.key, - GRAVITINO_ICEBERG_REST_URI.key, - GRAVITINO_ICEBERG_REST_ENABLED.key); } /** @@ -738,25 +709,40 @@ public class GravitinoConfig { } /** - * Returns whether lakehouse-iceberg catalogs are routed through the Gravitino Iceberg REST - * server. + * Sets the Iceberg REST server endpoint discovered from the Gravitino server for the given + * metalake. Called by the catalog connector manager's periodic metalake poll; ignored for a + * metalake where {@code gravitino.iceberg.rest-uri} is explicitly configured, which always takes + * precedence. * - * @return true if the Iceberg REST routing is enabled + * @param metalake the metalake the endpoint was discovered for + * @param uri the discovered endpoint, or {@code null} when the Iceberg REST server is not running + * or does not serve this metalake */ - public boolean isIcebergRestEnabled() { - return Boolean.parseBoolean( - config.getOrDefault( - GRAVITINO_ICEBERG_REST_ENABLED.key, GRAVITINO_ICEBERG_REST_ENABLED.defaultValue)); + public void setDiscoveredIcebergRestUri(String metalake, String uri) { + if (StringUtils.isBlank(uri)) { + discoveredIcebergRestUriByMetalake.remove(metalake); + } else { + discoveredIcebergRestUriByMetalake.put(metalake, uri); + } } /** - * Retrieves the endpoint of the Gravitino Iceberg REST server. + * Retrieves the Iceberg REST server endpoint to route the given metalake's lakehouse-iceberg + * catalogs through. Prefers an explicitly configured {@code gravitino.iceberg.rest-uri}; + * otherwise falls back to the endpoint discovered from the Gravitino server for this metalake, if + * any. * - * @return the Iceberg REST server endpoint, or an empty string if not configured + * @param metalake the metalake to resolve the endpoint for + * @return the Iceberg REST server endpoint, or an empty string when none is available */ - public String getIcebergRestUri() { - return config.getOrDefault( - GRAVITINO_ICEBERG_REST_URI.key, GRAVITINO_ICEBERG_REST_URI.defaultValue); + public String getIcebergRestUri(String metalake) { + String configured = + config.getOrDefault( + GRAVITINO_ICEBERG_REST_URI.key, GRAVITINO_ICEBERG_REST_URI.defaultValue); + if (StringUtils.isNotBlank(configured)) { + return configured; + } + return discoveredIcebergRestUriByMetalake.getOrDefault(metalake, ""); } /** diff --git a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogConnectorManager.java b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogConnectorManager.java index 34872d2e2f..e11c16b30b 100644 --- a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogConnectorManager.java +++ b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogConnectorManager.java @@ -197,6 +197,7 @@ public class CatalogConnectorManager { try { GravitinoMetalake metalake = metalakes.get(usedMetalake); LOG.debug("Load metalake: {}", usedMetalake); + refreshIcebergRestUri(usedMetalake); loadCatalogs(metalake); } catch (Exception e) { LOG.error("Load Metalake {} failed.", usedMetalake, e); @@ -207,6 +208,23 @@ public class CatalogConnectorManager { } } + /** + * Asks the Gravitino server whether it has an Iceberg REST server running for this metalake, and + * caches the answer on the shared {@link GravitinoConfig} for {@code IcebergConnectorAdapter} to + * read on the next catalog load. Failures — including talking to a Gravitino server older than + * this endpoint — must not interrupt catalog loading, so they are swallowed here; Iceberg + * catalogs simply keep their last known routing decision until the next successful poll. + */ + private void refreshIcebergRestUri(String metalakeName) { + try { + config.setDiscoveredIcebergRestUri( + metalakeName, gravitinoClient.icebergRestServiceUri(metalakeName).orElse(null)); + } catch (Exception e) { + LOG.debug( + "Failed to query the Iceberg REST service endpoint for metalake {}.", metalakeName, e); + } + } + /** * Retrieves a metalake by its name. * diff --git a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergCatalogPropertyConverter.java b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergCatalogPropertyConverter.java index dd8a492e1d..5d81fd02ab 100644 --- a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergCatalogPropertyConverter.java +++ b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergCatalogPropertyConverter.java @@ -151,18 +151,21 @@ public class IcebergCatalogPropertyConverter extends CatalogPropertyConverter { * @param catalog the Gravitino catalog to load * @param gravitinoConfig the connector configuration holding the Iceberg REST server endpoint * @return the Trino Iceberg connector config - * @throws TrinoException if {@code gravitino.iceberg.rest-uri} is not configured + * @throws TrinoException if no Iceberg REST server endpoint is configured or discovered for this + * catalog's metalake */ public Map<String, String> buildIcebergRestProperties( GravitinoCatalog catalog, GravitinoConfig gravitinoConfig) { - String restUri = gravitinoConfig.getIcebergRestUri(); + String restUri = gravitinoConfig.getIcebergRestUri(catalog.getMetalake()); if (StringUtils.isBlank(restUri)) { + // The caller only reaches this method once it has already confirmed a non-blank URI, so + // this is defensive: it can only fire if that URI disappeared between the two calls. throw new TrinoException( GravitinoErrorCode.GRAVITINO_MISSING_CONFIG, - "Missing required config 'gravitino.iceberg.rest-uri'. Set it to the Gravitino Iceberg " - + "REST server endpoint, for example http://localhost:9001/iceberg, or set " - + "'gravitino.iceberg.rest-enabled=false' to load Iceberg catalogs through their " - + "catalog backend instead."); + "No Iceberg REST server endpoint is available for metalake '" + + catalog.getMetalake() + + "'. Set 'gravitino.iceberg.rest-uri' to override the address the Gravitino " + + "server reports."); } Map<String, String> config = new HashMap<>(); @@ -189,19 +192,6 @@ public class IcebergCatalogPropertyConverter extends CatalogPropertyConverter { // The server echoes `prefix` back as a config default, so setting it here is belt-and-braces. config.put(TRINO_ICEBERG_REST_WAREHOUSE, catalog.getName()); config.put(TRINO_ICEBERG_REST_PREFIX, catalog.getName()); - - if (!gravitinoConfig.singleMetalakeMode()) { - // The IRC serves exactly one metalake, and the prefix carries only the catalog name, so two - // metalakes holding a same-named catalog both resolve to whichever one the IRC was started - // with. - LOG.warn( - "Catalog '{}' in metalake '{}' is routed to the Iceberg REST server using the catalog " - + "name alone. In multi-metalake mode this is ambiguous: the Iceberg REST server " - + "serves a single metalake, so a same-named catalog in another metalake resolves to " - + "the same prefix.", - catalog.getName(), - catalog.getMetalake()); - } return config; } diff --git a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergConnectorAdapter.java b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergConnectorAdapter.java index 1480dc13d4..4cdd9cd1c0 100644 --- a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergConnectorAdapter.java +++ b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/iceberg/IcebergConnectorAdapter.java @@ -24,6 +24,7 @@ import io.trino.spi.session.PropertyMetadata; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.commons.lang3.StringUtils; import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergConstants; import org.apache.gravitino.credential.Credential; import org.apache.gravitino.trino.connector.GravitinoConfig; @@ -64,10 +65,12 @@ public class IcebergConnectorAdapter implements CatalogConnectorAdapter { public Map<String, String> buildInternalConnectorConfig( GravitinoCatalog catalog, Credential[] credentials) throws Exception { // The catalog backend describes how Gravitino stores the metadata; it does not decide how - // Trino reaches the data. Unless the routing is disabled, every catalog is loaded through the - // Gravitino Iceberg REST server, the only path that supports temporary credentials. - // A catalog that already has a REST backend keeps pointing at its own configured endpoint. - if (config.isIcebergRestEnabled() + // Trino reaches the data. Whenever the Gravitino server reports a running Iceberg REST + // server for this catalog's metalake, the catalog is loaded through it, the only path that + // supports temporary credentials. A catalog that already has a REST backend keeps pointing at + // its own configured endpoint. If the server reports nothing, this falls back to translating + // catalog-backend as before — nothing to configure either way. + if (StringUtils.isNotBlank(config.getIcebergRestUri(catalog.getMetalake())) && !REST_CATALOG_BACKEND.equalsIgnoreCase( catalog.getProperty(IcebergConstants.CATALOG_BACKEND, null))) { // `credentials` is intentionally unused here: with vended credentials enabled, Trino obtains diff --git a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConfig.java b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConfig.java index 357280cf90..0528006823 100644 --- a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConfig.java +++ b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConfig.java @@ -304,8 +304,8 @@ public class TestGravitinoConfig { public void testIcebergRestConfigDefaults() { GravitinoConfig config = new GravitinoConfig(ImmutableMap.of("gravitino.metalake", "user_001")); - assertTrue(config.isIcebergRestEnabled()); - assertEquals("", config.getIcebergRestUri()); + // Nothing configured, and nothing discovered yet. + assertEquals("", config.getIcebergRestUri("user_001")); assertTrue(config.getIcebergRestCatalogConfig().isEmpty()); } @@ -315,8 +315,6 @@ public class TestGravitinoConfig { ImmutableMap.of( "gravitino.metalake", "user_001", - "gravitino.iceberg.rest-enabled", - "false", "gravitino.iceberg.rest-uri", "http://127.0.0.1:9001/iceberg", "gravitino.iceberg.rest-catalog.security", @@ -325,8 +323,9 @@ public class TestGravitinoConfig { "client_id:client_secret"); GravitinoConfig config = new GravitinoConfig(configMap); - assertFalse(config.isIcebergRestEnabled()); - assertEquals("http://127.0.0.1:9001/iceberg", config.getIcebergRestUri()); + // A manually configured URI applies regardless of metalake. + assertEquals("http://127.0.0.1:9001/iceberg", config.getIcebergRestUri("user_001")); + assertEquals("http://127.0.0.1:9001/iceberg", config.getIcebergRestUri("other_metalake")); Map<String, String> restCatalogConfig = config.getIcebergRestCatalogConfig(); assertEquals(2, restCatalogConfig.size()); @@ -335,6 +334,38 @@ public class TestGravitinoConfig { "client_id:client_secret", restCatalogConfig.get("iceberg.rest-catalog.oauth2.credential")); } + @Test + public void testDiscoveredIcebergRestUriIsPerMetalake() { + GravitinoConfig config = new GravitinoConfig(ImmutableMap.of("gravitino.metalake", "user_001")); + + config.setDiscoveredIcebergRestUri("metalake_a", "http://irc-a:9001/iceberg"); + config.setDiscoveredIcebergRestUri("metalake_b", "http://irc-b:9001/iceberg"); + + assertEquals("http://irc-a:9001/iceberg", config.getIcebergRestUri("metalake_a")); + assertEquals("http://irc-b:9001/iceberg", config.getIcebergRestUri("metalake_b")); + // Unknown/unmatched metalakes fall back to nothing. + assertEquals("", config.getIcebergRestUri("metalake_c")); + + // Clearing (e.g. the server stopped reporting an endpoint) removes it again. + config.setDiscoveredIcebergRestUri("metalake_a", null); + assertEquals("", config.getIcebergRestUri("metalake_a")); + } + + @Test + public void testManualIcebergRestUriOverridesDiscovery() { + GravitinoConfig config = + new GravitinoConfig( + ImmutableMap.of( + "gravitino.metalake", + "user_001", + "gravitino.iceberg.rest-uri", + "http://manual:9001/iceberg")); + + config.setDiscoveredIcebergRestUri("user_001", "http://discovered:9001/iceberg"); + + assertEquals("http://manual:9001/iceberg", config.getIcebergRestUri("user_001")); + } + @Test public void testToCatalogConfigWithIcebergRestProperties() { ImmutableMap<String, String> configMap = @@ -353,18 +384,6 @@ public class TestGravitinoConfig { assertTrue(catalogConfig.contains("\"gravitino.iceberg.rest-catalog.security\"='OAUTH2'")); } - @Test - public void testToCatalogConfigPropagatesIcebergRestEnabled() { - // The switch rides the exact-key loop rather than the prefix filter; if it fails to propagate, - // the coordinator and the workers build different configs for the same catalog. - GravitinoConfig config = - new GravitinoConfig( - ImmutableMap.of( - "gravitino.metalake", "user_001", "gravitino.iceberg.rest-enabled", "false")); - - assertTrue(config.toCatalogConfig().contains("\"gravitino.iceberg.rest-enabled\"='false'")); - } - private static boolean skipCatalog(String catalogName, GravitinoConfig config) { for (Pattern pattern : config.getSkipCatalogPatterns()) { if (pattern.matcher(catalogName).matches()) { diff --git a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/iceberg/TestIcebergCatalogPropertyConverter.java b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/iceberg/TestIcebergCatalogPropertyConverter.java index f09e4d7469..8ad738188f 100644 --- a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/iceberg/TestIcebergCatalogPropertyConverter.java +++ b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/iceberg/TestIcebergCatalogPropertyConverter.java @@ -138,7 +138,7 @@ public class TestIcebergCatalogPropertyConverter { Catalog mockCatalog = TestGravitinoCatalog.mockCatalog( name, "lakehouse-iceberg", "test catalog", Catalog.Type.RELATIONAL, properties); - IcebergConnectorAdapter adapter = new IcebergConnectorAdapter(icebergRestDisabledConfig()); + IcebergConnectorAdapter adapter = new IcebergConnectorAdapter(icebergRestUnavailableConfig()); Map<String, String> config = adapter.buildInternalConnectorConfig( @@ -178,7 +178,7 @@ public class TestIcebergCatalogPropertyConverter { Catalog mockCatalog = TestGravitinoCatalog.mockCatalog( name, "lakehouse-iceberg", "test catalog", Catalog.Type.RELATIONAL, properties); - IcebergConnectorAdapter adapter = new IcebergConnectorAdapter(icebergRestDisabledConfig()); + IcebergConnectorAdapter adapter = new IcebergConnectorAdapter(icebergRestUnavailableConfig()); Map<String, String> config = adapter.buildInternalConnectorConfig( @@ -215,7 +215,7 @@ public class TestIcebergCatalogPropertyConverter { Catalog mockCatalog = TestGravitinoCatalog.mockCatalog( name, "lakehouse-iceberg", "test catalog", Catalog.Type.RELATIONAL, properties); - IcebergConnectorAdapter adapter = new IcebergConnectorAdapter(icebergRestDisabledConfig()); + IcebergConnectorAdapter adapter = new IcebergConnectorAdapter(icebergRestUnavailableConfig()); Map<String, String> config = adapter.buildInternalConnectorConfig( @@ -242,7 +242,8 @@ public class TestIcebergCatalogPropertyConverter { .build(); Map<String, String> config = - buildConnectorConfig("catalog1", properties, icebergRestEnabledConfig(ImmutableMap.of())); + buildConnectorConfig( + "catalog1", properties, icebergRestConfiguredConfig(ImmutableMap.of())); Assertions.assertEquals("rest", config.get("iceberg.catalog.type")); Assertions.assertEquals( @@ -268,7 +269,8 @@ public class TestIcebergCatalogPropertyConverter { .build(); Map<String, String> config = - buildConnectorConfig("catalog1", properties, icebergRestEnabledConfig(ImmutableMap.of())); + buildConnectorConfig( + "catalog1", properties, icebergRestConfiguredConfig(ImmutableMap.of())); Assertions.assertEquals("rest", config.get("iceberg.catalog.type")); Assertions.assertEquals("catalog1", config.get("iceberg.rest-catalog.prefix")); @@ -285,7 +287,8 @@ public class TestIcebergCatalogPropertyConverter { .build(); Map<String, String> config = - buildConnectorConfig("catalog1", properties, icebergRestEnabledConfig(ImmutableMap.of())); + buildConnectorConfig( + "catalog1", properties, icebergRestConfiguredConfig(ImmutableMap.of())); Assertions.assertEquals("rest", config.get("iceberg.catalog.type")); Assertions.assertEquals( @@ -305,7 +308,7 @@ public class TestIcebergCatalogPropertyConverter { .build(); Map<String, String> config = - buildConnectorConfig("catalog1", properties, icebergRestDisabledConfig()); + buildConnectorConfig("catalog1", properties, icebergRestUnavailableConfig()); Assertions.assertEquals("jdbc", config.get("iceberg.catalog.type")); Assertions.assertEquals( @@ -315,18 +318,85 @@ public class TestIcebergCatalogPropertyConverter { } @Test - public void testBuildConnectorPropertiesMissingIcebergRestUri() { + public void testBuildConnectorPropertiesFallsBackWhenNoIcebergRestUriIsAvailable() + throws Exception { Map<String, String> properties = ImmutableMap.<String, String>builder() .put("uri", "jdbc:postgresql://localhost:5432/iceberg") .put("catalog-backend", "jdbc") .put("jdbc-driver", "org.postgresql.Driver") .build(); - GravitinoConfig gravitinoConfig = - new GravitinoConfig(ImmutableMap.of("gravitino.metalake", "test")); + + // Neither a manual gravitino.iceberg.rest-uri nor a discovered one for this metalake: the + // routing gate never fires, so this is the ordinary backend-translation path, not an error. + Map<String, String> config = + buildConnectorConfig("catalog1", properties, icebergRestUnavailableConfig()); + + Assertions.assertEquals("jdbc", config.get("iceberg.catalog.type")); + Assertions.assertNull(config.get("iceberg.rest-catalog.uri")); + } + + @Test + public void testBuildIcebergRestPropertiesThrowsOnBlankUri() { + // buildIcebergRestProperties keeps its own defensive check even though + // IcebergConnectorAdapter's routing gate means callers can no longer reach it with a blank + // URI under normal operation. + Catalog mockCatalog = + TestGravitinoCatalog.mockCatalog( + "catalog1", + "lakehouse-iceberg", + "test catalog", + Catalog.Type.RELATIONAL, + ImmutableMap.of("catalog-backend", "jdbc")); + GravitinoCatalog catalog = new GravitinoCatalog("test", mockCatalog); + IcebergCatalogPropertyConverter converter = new IcebergCatalogPropertyConverter(); Assertions.assertThrows( - TrinoException.class, () -> buildConnectorConfig("catalog1", properties, gravitinoConfig)); + TrinoException.class, + () -> converter.buildIcebergRestProperties(catalog, icebergRestUnavailableConfig())); + } + + @Test + public void testBuildConnectorPropertiesRoutesThroughDiscoveredIcebergRestUri() throws Exception { + Map<String, String> properties = + ImmutableMap.<String, String>builder() + .put("catalog-backend", "jdbc") + .put("uri", "jdbc:postgresql://localhost:5432/iceberg") + .put("jdbc-driver", "org.postgresql.Driver") + .build(); + + // No manual gravitino.iceberg.rest-uri: only a per-metalake value discovered from the + // Gravitino server, exactly as CatalogConnectorManager's periodic poll would set it. + Map<String, String> config = + buildConnectorConfig( + "catalog1", + properties, + icebergRestDiscoveredConfig("test", "http://discovered:9001/iceberg")); + + Assertions.assertEquals("rest", config.get("iceberg.catalog.type")); + Assertions.assertEquals( + "http://discovered:9001/iceberg", config.get("iceberg.rest-catalog.uri")); + } + + @Test + public void testBuildConnectorPropertiesIgnoresDiscoveryForOtherMetalakes() throws Exception { + Map<String, String> properties = + ImmutableMap.<String, String>builder() + .put("catalog-backend", "jdbc") + .put("uri", "jdbc:postgresql://localhost:5432/iceberg") + .put("jdbc-driver", "org.postgresql.Driver") + .build(); + + // Discovery reported an endpoint for a different metalake than the one this catalog belongs + // to (the mock catalog's metalake is "test"); it must not leak across metalakes. + Map<String, String> config = + buildConnectorConfig( + "catalog1", + properties, + icebergRestDiscoveredConfig("other_metalake", "http://other:9001/iceberg")); + + Assertions.assertEquals("jdbc", config.get("iceberg.catalog.type")); + Assertions.assertNull(config.get("iceberg.rest-catalog.uri")); } @Test @@ -339,7 +409,7 @@ public class TestIcebergCatalogPropertyConverter { .put("warehouse", "s3://bucket/warehouse/") .build(); GravitinoConfig gravitinoConfig = - icebergRestEnabledConfig( + icebergRestConfiguredConfig( ImmutableMap.of( "gravitino.iceberg.rest-catalog.security", "OAUTH2", "gravitino.iceberg.rest-catalog.oauth2.credential", "client_id:client_secret", @@ -368,7 +438,7 @@ public class TestIcebergCatalogPropertyConverter { buildConnectorConfig( "catalog1", properties, - icebergRestEnabledConfig( + icebergRestConfiguredConfig( ImmutableMap.of("gravitino.client.session.forwardUser", "true"))); Assertions.assertEquals("USER", config.get("iceberg.rest-catalog.session")); @@ -376,14 +446,15 @@ public class TestIcebergCatalogPropertyConverter { buildConnectorConfig( "catalog1", properties, - icebergRestEnabledConfig( + icebergRestConfiguredConfig( ImmutableMap.of( "gravitino.client.session.forwardUser", "true", "gravitino.iceberg.rest-catalog.session", "NONE"))); Assertions.assertEquals("NONE", explicitConfig.get("iceberg.rest-catalog.session")); Map<String, String> defaultConfig = - buildConnectorConfig("catalog1", properties, icebergRestEnabledConfig(ImmutableMap.of())); + buildConnectorConfig( + "catalog1", properties, icebergRestConfiguredConfig(ImmutableMap.of())); Assertions.assertNull(defaultConfig.get("iceberg.rest-catalog.session")); } @@ -397,7 +468,7 @@ public class TestIcebergCatalogPropertyConverter { "warehouse", "s3://bucket/warehouse/", "s3-region", "us-east-1", "s3-endpoint", "http://minio:9000"), - icebergRestEnabledConfig(ImmutableMap.of())); + icebergRestConfiguredConfig(ImmutableMap.of())); Assertions.assertEquals("true", s3Config.get("fs.native-s3.enabled")); Assertions.assertEquals("us-east-1", s3Config.get("s3.region")); Assertions.assertEquals("http://minio:9000", s3Config.get("s3.endpoint")); @@ -407,7 +478,7 @@ public class TestIcebergCatalogPropertyConverter { buildConnectorConfig( "catalog1", ImmutableMap.of("catalog-backend", "jdbc", "warehouse", "gs://bucket/warehouse/"), - icebergRestEnabledConfig(ImmutableMap.of())); + icebergRestConfiguredConfig(ImmutableMap.of())); Assertions.assertEquals("true", gcsConfig.get("fs.native-gcs.enabled")); Assertions.assertNull(gcsConfig.get("fs.native-s3.enabled")); @@ -416,14 +487,14 @@ public class TestIcebergCatalogPropertyConverter { "catalog1", ImmutableMap.of( "catalog-backend", "jdbc", "warehouse", "abfss://container@account/warehouse/"), - icebergRestEnabledConfig(ImmutableMap.of())); + icebergRestConfiguredConfig(ImmutableMap.of())); Assertions.assertEquals("true", azureConfig.get("fs.native-azure.enabled")); Map<String, String> hdfsConfig = buildConnectorConfig( "catalog1", ImmutableMap.of("catalog-backend", "jdbc", "warehouse", "hdfs://namenode:9000/wh"), - icebergRestEnabledConfig(ImmutableMap.of())); + icebergRestConfiguredConfig(ImmutableMap.of())); Assertions.assertEquals("true", hdfsConfig.get("fs.hadoop.enabled")); Assertions.assertNull(hdfsConfig.get("fs.native-s3.enabled")); Assertions.assertNull(hdfsConfig.get("fs.native-gcs.enabled")); @@ -441,7 +512,8 @@ public class TestIcebergCatalogPropertyConverter { .build(); Map<String, String> config = - buildConnectorConfig("catalog1", properties, icebergRestEnabledConfig(ImmutableMap.of())); + buildConnectorConfig( + "catalog1", properties, icebergRestConfiguredConfig(ImmutableMap.of())); Assertions.assertEquals("true", config.get("iceberg.table-statistics-enabled")); // A catalog can override a derived default, so a Trino release renaming it is not a blocker. @@ -470,7 +542,7 @@ public class TestIcebergCatalogPropertyConverter { Map<String, String> config = buildConnectorConfig( - "catalog1", properties, icebergRestEnabledConfig(ImmutableMap.of()), credentials); + "catalog1", properties, icebergRestConfiguredConfig(ImmutableMap.of()), credentials); // The REST protocol vends a fresh credential per table access, so the catalog-level snapshot // must not be pinned into the connector config. @@ -482,7 +554,7 @@ public class TestIcebergCatalogPropertyConverter { // The backend path still applies them. Map<String, String> legacyConfig = - buildConnectorConfig("catalog1", properties, icebergRestDisabledConfig(), credentials); + buildConnectorConfig("catalog1", properties, icebergRestUnavailableConfig(), credentials); Assertions.assertEquals("root", legacyConfig.get("iceberg.jdbc-catalog.connection-user")); Assertions.assertEquals("AKIAEXAMPLE", legacyConfig.get("hive.s3.aws-access-key")); } @@ -500,7 +572,8 @@ public class TestIcebergCatalogPropertyConverter { .build(); Map<String, String> config = - buildConnectorConfig("catalog1", properties, icebergRestEnabledConfig(ImmutableMap.of())); + buildConnectorConfig( + "catalog1", properties, icebergRestConfiguredConfig(ImmutableMap.of())); // A catalog property must never redirect the connector at another endpoint or catalog. Assertions.assertEquals("rest", config.get("iceberg.catalog.type")); @@ -523,7 +596,7 @@ public class TestIcebergCatalogPropertyConverter { buildConnectorConfig( "catalog1", properties, - icebergRestEnabledConfig( + icebergRestConfiguredConfig( ImmutableMap.of("gravitino.iceberg.rest-catalog.security", "OAUTH2"))); // Cluster-level operational settings outrank per-catalog properties. @@ -538,7 +611,7 @@ public class TestIcebergCatalogPropertyConverter { buildConnectorConfig( "catalog1", ImmutableMap.of("catalog-backend", "jdbc", "warehouse", warehouse), - icebergRestEnabledConfig(ImmutableMap.of())); + icebergRestConfiguredConfig(ImmutableMap.of())); Assertions.assertEquals( "true", config.get("fs.native-s3.enabled"), "warehouse: " + warehouse); } @@ -548,7 +621,7 @@ public class TestIcebergCatalogPropertyConverter { buildConnectorConfig( "catalog1", ImmutableMap.of("catalog-backend", "jdbc", "warehouse", "gt_iceberg_rest"), - icebergRestEnabledConfig(ImmutableMap.of())); + icebergRestConfiguredConfig(ImmutableMap.of())); Assertions.assertEquals("true", schemeless.get("fs.hadoop.enabled")); Assertions.assertNull(schemeless.get("fs.native-s3.enabled")); @@ -557,7 +630,7 @@ public class TestIcebergCatalogPropertyConverter { buildConnectorConfig( "catalog1", ImmutableMap.of("catalog-backend", "jdbc"), - icebergRestEnabledConfig(ImmutableMap.of())); + icebergRestConfiguredConfig(ImmutableMap.of())); Assertions.assertEquals("true", noWarehouse.get("fs.hadoop.enabled")); Assertions.assertNull(noWarehouse.get("fs.native-s3.enabled")); Assertions.assertNull(noWarehouse.get("fs.native-gcs.enabled")); @@ -571,7 +644,7 @@ public class TestIcebergCatalogPropertyConverter { "catalog-backend", "jdbc", "warehouse", "oss://bucket/wh", "credential-providers", "oss-token"), - icebergRestEnabledConfig(ImmutableMap.of())); + icebergRestConfiguredConfig(ImmutableMap.of())); Assertions.assertEquals("true", oss.get("fs.hadoop.enabled")); Assertions.assertNull(oss.get("fs.native-s3.enabled")); } @@ -586,7 +659,7 @@ public class TestIcebergCatalogPropertyConverter { "warehouse", "s3://bucket/wh", "s3-endpoint", "http://minio:9000", "s3-path-style-access", "true"), - icebergRestEnabledConfig(ImmutableMap.of())); + icebergRestConfiguredConfig(ImmutableMap.of())); // S3-compatible stores such as MinIO need path-style addressing to resolve the bucket. Assertions.assertEquals("true", config.get("s3.path-style-access")); @@ -599,7 +672,7 @@ public class TestIcebergCatalogPropertyConverter { buildConnectorConfig( "catalog1", ImmutableMap.of("catalog-backend", "REST", "uri", "http://other-irc:9001/iceberg"), - icebergRestEnabledConfig(ImmutableMap.of())); + icebergRestConfiguredConfig(ImmutableMap.of())); Assertions.assertEquals( "http://other-irc:9001/iceberg", config.get("iceberg.rest-catalog.uri")); @@ -619,12 +692,13 @@ public class TestIcebergCatalogPropertyConverter { .buildInternalConnectorConfig(new GravitinoCatalog("test", mockCatalog), credentials); } - private static GravitinoConfig icebergRestDisabledConfig() { - return new GravitinoConfig( - ImmutableMap.of("gravitino.metalake", "test", "gravitino.iceberg.rest-enabled", "false")); + private static GravitinoConfig icebergRestUnavailableConfig() { + // No manual gravitino.iceberg.rest-uri, and nothing discovered for this metalake: the + // connector has no endpoint to route through, so catalogs fall back to their own backend. + return new GravitinoConfig(ImmutableMap.of("gravitino.metalake", "test")); } - private static GravitinoConfig icebergRestEnabledConfig(Map<String, String> extraConfig) { + private static GravitinoConfig icebergRestConfiguredConfig(Map<String, String> extraConfig) { return new GravitinoConfig( ImmutableMap.<String, String>builder() .put("gravitino.metalake", "test") @@ -632,4 +706,10 @@ public class TestIcebergCatalogPropertyConverter { .putAll(extraConfig) .build()); } + + private static GravitinoConfig icebergRestDiscoveredConfig(String metalake, String uri) { + GravitinoConfig config = new GravitinoConfig(ImmutableMap.of("gravitino.metalake", metalake)); + config.setDiscoveredIcebergRestUri(metalake, uri); + return config; + } }
