This is an automated email from the ASF dual-hosted git repository.
jerryshao pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-1.3 by this push:
new 544cf8a5df [Cherry-pick to branch-1.3] [MINOR] refactor(server): Share
Iceberg REST service URI resolution (#13738) (#13742)
544cf8a5df is described below
commit 544cf8a5dfd1b595e67c130379920db6b8a3e594
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sat Oct 10 18:47:15 2026 +0800
[Cherry-pick to branch-1.3] [MINOR] refactor(server): Share Iceberg REST
service URI resolution (#13738) (#13742)
**Cherry-pick Information:**
- Original commit: ddbc9a20c7258ff81b5d6195382434ecf81f4db5
- Target branch: `branch-1.3`
- Status: ✅ Clean cherry-pick (no conflicts)
Co-authored-by: mchades <[email protected]>
---
.../org/apache/gravitino/server/web/Utils.java | 161 +++++++++++++++++++++
.../org/apache/gravitino/server/web/TestUtils.java | 155 ++++++++++++++++++++
.../web/rest/IcebergRESTServiceOperations.java | 149 +------------------
3 files changed, 323 insertions(+), 142 deletions(-)
diff --git
a/server-common/src/main/java/org/apache/gravitino/server/web/Utils.java
b/server-common/src/main/java/org/apache/gravitino/server/web/Utils.java
index 43e8d41568..3b03ae88fa 100644
--- a/server-common/src/main/java/org/apache/gravitino/server/web/Utils.java
+++ b/server-common/src/main/java/org/apache/gravitino/server/web/Utils.java
@@ -20,11 +20,14 @@ package org.apache.gravitino.server.web;
import com.google.common.collect.Maps;
import java.lang.reflect.Parameter;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.security.PrivilegedExceptionAction;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
+import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.MediaType;
@@ -37,16 +40,45 @@ import org.apache.gravitino.audit.FilesetAuditConstants;
import org.apache.gravitino.audit.FilesetDataOperation;
import org.apache.gravitino.audit.InternalClientType;
import org.apache.gravitino.auth.AuthConstants;
+import org.apache.gravitino.auxiliary.AuxiliaryServiceManager;
import org.apache.gravitino.credential.CredentialConstants;
import org.apache.gravitino.dto.HealthCheckDTO;
import org.apache.gravitino.dto.responses.ErrorResponse;
import org.apache.gravitino.dto.responses.HealthResponse;
import org.apache.gravitino.utils.PrincipalUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class Utils {
+ private static final Logger LOG = LoggerFactory.getLogger(Utils.class);
private static final String REMOTE_USER = "gravitino";
+ // Matches gravitino.auxService.names / AuxiliaryServiceManager's
registration key.
+ private static final String ICEBERG_REST_SERVICE_NAME = "iceberg-rest";
+ // Keys below are read from AuxiliaryServiceManager.getAuxServiceConfig,
which already strips
+ // the gravitino.iceberg-rest. (or deprecated
gravitino.auxService.iceberg-rest.) prefix, so
+ // they must NOT be re-prefixed here.
+ // The provider name used by the Iceberg REST server itself; see
+ // IcebergConstants.ICEBERG_REST_CATALOG_CONFIG_PROVIDER and
DynamicIcebergConfigProvider. The
+ // shared server utilities do not depend on Iceberg implementation modules.
+ private static final String ICEBERG_CATALOG_CONFIG_PROVIDER_KEY =
"catalog-config-provider";
+ private static final String ICEBERG_DYNAMIC_CONFIG_PROVIDER_NAME =
"dynamic-config-provider";
+ // The post-strip key used by the Iceberg REST server itself; see
+ // IcebergConstants.GRAVITINO_METALAKE and DynamicIcebergConfigProvider.
+ private static final String ICEBERG_SERVED_METALAKE_KEY =
"gravitino-metalake";
+ // Overrides the listener-derived endpoint; see docs/iceberg-rest-service.md.
+ private static final String ICEBERG_ADVERTISED_URI_KEY = "advertised-uri";
+ private static final String ICEBERG_HOST_KEY = "host";
+ private static final String ICEBERG_HTTP_PORT_KEY = "httpPort";
+ private static final String ICEBERG_HTTPS_PORT_KEY = "httpsPort";
+ private static final String ICEBERG_ENABLE_HTTPS_KEY = "enableHttps";
+ // Match IcebergConfig.DEFAULT_ICEBERG_REST_SERVICE_HTTP_PORT/HTTPS_PORT.
JettyServerConfig's
+ // defaults belong to the Gravitino server (8090/8433), not the Iceberg REST
server.
+ private static final int ICEBERG_DEFAULT_HTTP_PORT = 9001;
+ private static final int ICEBERG_DEFAULT_HTTPS_PORT = 9433;
+ private static final String ICEBERG_DEFAULT_HOST = "0.0.0.0";
+
private Utils() {}
public static String remoteUser(HttpServletRequest httpRequest) {
@@ -340,4 +372,133 @@ public class Utils {
}
return pathParams;
}
+
+ /**
+ * Resolves the Iceberg REST service URI from the supplied service
registration and configuration.
+ *
+ * <p>A valid advertised URI takes precedence over the listener
configuration. Otherwise, the URI
+ * uses the configured host, protocol and port, with Iceberg REST defaults
for omitted values. A
+ * wildcard listener host is replaced with {@code requestHost}, assuming
both services share a
+ * host. Reverse proxies or split deployments should configure {@code
advertised-uri} explicitly.
+ *
+ * @param serviceManager the auxiliary service manager used to check service
registration
+ * @param serviceConfig the effective Iceberg REST service configuration
returned by {@link
+ * AuxiliaryServiceManager#getAuxServiceConfig}, with the service prefix
stripped
+ * @param metalake the requested metalake, or null/blank to skip the
metalake check
+ * @param requestHost the hostname from the current request, used for
wildcard listener hosts
+ * @return the service URI, or null if the service is not registered, does
not use the dynamic
+ * catalog config provider, or serves a different metalake
+ * @throws IllegalStateException if the advertised URI is invalid
+ */
+ @Nullable
+ public static String resolveIcebergRestServiceUri(
+ AuxiliaryServiceManager serviceManager,
+ Map<String, String> serviceConfig,
+ @Nullable String metalake,
+ String requestHost) {
+ if (!serviceManager.isAuxServiceRegistered(ICEBERG_REST_SERVICE_NAME)) {
+ return null;
+ }
+
+ String provider =
serviceConfig.getOrDefault(ICEBERG_CATALOG_CONFIG_PROVIDER_KEY, "");
+ if (!ICEBERG_DYNAMIC_CONFIG_PROVIDER_NAME.equals(provider)) {
+ // Only the dynamic catalog config provider maps Iceberg REST catalog
names onto Gravitino
+ // catalogs; the default static provider serves statically-declared
catalogs unrelated to
+ // Gravitino catalog names, so routing at it would 404 on every request.
+ LOG.debug(
+ "Iceberg REST service does not use the dynamic catalog config
provider "
+ + "(catalog-config-provider={}); not reporting its endpoint for
auto-discovery.",
+ provider);
+ return null;
+ }
+
+ String servedMetalake =
serviceConfig.getOrDefault(ICEBERG_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.
+ LOG.debug(
+ "Iceberg REST service serves metalake {}, not the requested metalake
{}; not "
+ + "reporting its endpoint for auto-discovery.",
+ servedMetalake,
+ metalake);
+ return null;
+ }
+
+ String advertisedUri =
StringUtils.trimToNull(serviceConfig.get(ICEBERG_ADVERTISED_URI_KEY));
+ if (advertisedUri != null) {
+ return checkIcebergRestAdvertisedUri(advertisedUri);
+ }
+
+ String host = serviceConfig.getOrDefault(ICEBERG_HOST_KEY,
ICEBERG_DEFAULT_HOST);
+ if (isWildcardHost(host)) {
+ host = requestHost;
+ }
+ boolean enableHttps =
+
Boolean.parseBoolean(serviceConfig.getOrDefault(ICEBERG_ENABLE_HTTPS_KEY,
"false"));
+ String scheme = enableHttps ? "https" : "http";
+ int port =
+ parseIcebergRestPort(
+ serviceConfig,
+ enableHttps ? ICEBERG_HTTPS_PORT_KEY : ICEBERG_HTTP_PORT_KEY,
+ enableHttps ? ICEBERG_DEFAULT_HTTPS_PORT :
ICEBERG_DEFAULT_HTTP_PORT);
+ return String.format("%s://%s:%d/iceberg", scheme, bracketIfIPv6(host),
port);
+ }
+
+ private static String checkIcebergRestAdvertisedUri(String value) {
+ boolean valid;
+ try {
+ URI uri = new URI(value);
+ // URI accepts any non-negative integer as a port; -1 means no explicit
port.
+ int port = uri.getPort();
+ valid =
+ StringUtils.equalsAnyIgnoreCase(uri.getScheme(), "http", "https")
+ && StringUtils.isNotBlank(uri.getHost())
+ && (port == -1 || (port >= 1 && port <= 65535))
+ && uri.getQuery() == null
+ && uri.getFragment() == null;
+ } catch (URISyntaxException e) {
+ valid = false;
+ }
+ if (!valid) {
+ throw new IllegalStateException(
+ String.format(
+ "Invalid Iceberg REST service %s '%s': expected an absolute
http(s) URI with a "
+ + "host, a port in 1-65535 if present, and no query or
fragment",
+ ICEBERG_ADVERTISED_URI_KEY, value));
+ }
+ return value;
+ }
+
+ // An IPv6 literal host (e.g. "::1", from an explicit config value or from
+ // HttpServletRequest#getServerName()) must be bracketed to form a valid URI
authority;
+ // otherwise its colons are parsed as the port separator. A hostname or IPv4
address never
+ // contains a colon, so this only ever fires for IPv6.
+ private static String bracketIfIPv6(String host) {
+ if (host.contains(":") && !host.startsWith("[")) {
+ return "[" + host + "]";
+ }
+ return host;
+ }
+
+ private static int parseIcebergRestPort(Map<String, String> config, String
key, int defaultPort) {
+ String value = config.getOrDefault(key, "");
+ if (StringUtils.isBlank(value)) {
+ return defaultPort;
+ }
+ try {
+ return Integer.parseInt(value.trim());
+ } catch (NumberFormatException e) {
+ return defaultPort;
+ }
+ }
+
+ private static boolean isWildcardHost(String host) {
+ return StringUtils.isBlank(host)
+ || "0.0.0.0".equals(host)
+ || "::".equals(host)
+ || "[::]".equals(host)
+ || "0:0:0:0:0:0:0:0".equals(host);
+ }
}
diff --git
a/server-common/src/test/java/org/apache/gravitino/server/web/TestUtils.java
b/server-common/src/test/java/org/apache/gravitino/server/web/TestUtils.java
index 0e8d5023b3..877a727f86 100644
--- a/server-common/src/test/java/org/apache/gravitino/server/web/TestUtils.java
+++ b/server-common/src/test/java/org/apache/gravitino/server/web/TestUtils.java
@@ -20,6 +20,8 @@ package org.apache.gravitino.server.web;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -30,11 +32,14 @@ import javax.ws.rs.core.Response;
import org.apache.gravitino.audit.FilesetAuditConstants;
import org.apache.gravitino.audit.FilesetDataOperation;
import org.apache.gravitino.audit.InternalClientType;
+import org.apache.gravitino.auxiliary.AuxiliaryServiceManager;
import org.apache.gravitino.dto.responses.ErrorConstants;
import org.apache.gravitino.dto.responses.ErrorResponse;
import org.apache.gravitino.exceptions.UnmodifiableStatisticException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
/*
@@ -236,4 +241,154 @@ public class TestUtils {
FilesetDataOperation.GET_FILE_STATUS.name(),
filteredMap.get(FilesetAuditConstants.HTTP_HEADER_FILESET_DATA_OPERATION));
}
+
+ @Test
+ void testIcebergRestServiceUriRequiresRegisteredService() {
+ assertNull(
+ Utils.resolveIcebergRestServiceUri(
+ mockAuxServiceManager(false),
+ Map.of(
+ "catalog-config-provider", "dynamic-config-provider",
+ "advertised-uri", "invalid"),
+ "test",
+ "gravitino-host"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"", "static-config-provider"})
+ void testIcebergRestServiceUriRequiresDynamicProvider(String provider) {
+ assertNull(
+ Utils.resolveIcebergRestServiceUri(
+ mockAuxServiceManager(true),
+ Map.of("catalog-config-provider", provider, "advertised-uri",
"invalid"),
+ "test",
+ "gravitino-host"));
+ }
+
+ @Test
+ void testIcebergRestServiceUriMatchesRequestedMetalake() {
+ AuxiliaryServiceManager serviceManager = mockAuxServiceManager(true);
+ String advertisedUri = "https://iceberg.example.com/proxy/iceberg/";
+ Map<String, String> config =
+ Map.of(
+ "catalog-config-provider", "dynamic-config-provider",
+ "gravitino-metalake", "prod",
+ "advertised-uri", advertisedUri);
+
+ assertNull(
+ Utils.resolveIcebergRestServiceUri(serviceManager, config, "test",
"gravitino-host"));
+ assertEquals(
+ advertisedUri,
+ Utils.resolveIcebergRestServiceUri(serviceManager, config, "prod",
"gravitino-host"));
+ assertEquals(
+ advertisedUri,
+ Utils.resolveIcebergRestServiceUri(serviceManager, config, null,
"gravitino-host"));
+ assertEquals(
+ advertisedUri,
+ Utils.resolveIcebergRestServiceUri(serviceManager, config, "",
"gravitino-host"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"", " ", "0.0.0.0", "::", "[::]", "0:0:0:0:0:0:0:0"})
+ void testIcebergRestServiceUriUsesProvidedRequestHost(String listenerHost) {
+ assertEquals(
+ "http://[::1]:9001/iceberg",
+ Utils.resolveIcebergRestServiceUri(
+ mockAuxServiceManager(true),
+ Map.of("catalog-config-provider", "dynamic-config-provider",
"host", listenerHost),
+ "test",
+ "::1"));
+ }
+
+ @Test
+ void testIcebergRestServiceUriUsesListenerConfiguration() {
+ assertEquals(
+ "https://iceberg-host:19433/iceberg",
+ Utils.resolveIcebergRestServiceUri(
+ mockAuxServiceManager(true),
+ Map.of(
+ "catalog-config-provider", "dynamic-config-provider",
+ "host", "iceberg-host",
+ "enableHttps", "true",
+ "httpPort", "19001",
+ "httpsPort", "19433"),
+ "test",
+ "gravitino-host"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testIcebergRestServiceUriUsesDefaultPorts(boolean enableHttps) {
+ assertEquals(
+ enableHttps ? "https://gravitino-host:9433/iceberg" :
"http://gravitino-host:9001/iceberg",
+ Utils.resolveIcebergRestServiceUri(
+ mockAuxServiceManager(true),
+ Map.of(
+ "catalog-config-provider",
+ "dynamic-config-provider",
+ "enableHttps",
+ Boolean.toString(enableHttps)),
+ "test",
+ "gravitino-host"));
+ }
+
+ @Test
+ void testIcebergRestServiceUriPrefersAdvertisedUri() {
+ assertEquals(
+ "https://iceberg.example.com:8443/proxy/iceberg/",
+ Utils.resolveIcebergRestServiceUri(
+ mockAuxServiceManager(true),
+ Map.of(
+ "catalog-config-provider", "dynamic-config-provider",
+ "host", "iceberg-host",
+ "httpPort", "19001",
+ "advertised-uri", "
https://iceberg.example.com:8443/proxy/iceberg/ "),
+ "test",
+ "gravitino-host"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "iceberg.example.com/iceberg",
+ "ftp://iceberg.example.com/iceberg",
+ "https:///iceberg",
+ "https://iceberg.example.com/iceberg?x=1",
+ "https://iceberg.example.com/iceberg#frag",
+ "https://iceberg.example.com:0/iceberg",
+ "https://iceberg.example.com:70000/iceberg",
+ "http://bad host/iceberg"
+ })
+ void testIcebergRestServiceUriRejectsInvalidAdvertisedUri(String
advertisedUri) {
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ Utils.resolveIcebergRestServiceUri(
+ mockAuxServiceManager(true),
+ Map.of(
+ "catalog-config-provider",
+ "dynamic-config-provider",
+ "advertised-uri",
+ advertisedUri),
+ "test",
+ "gravitino-host"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"", " ", "not-a-number"})
+ void testIcebergRestServiceUriUsesDefaultForInvalidPort(String port) {
+ assertEquals(
+ "http://gravitino-host:9001/iceberg",
+ Utils.resolveIcebergRestServiceUri(
+ mockAuxServiceManager(true),
+ Map.of("catalog-config-provider", "dynamic-config-provider",
"httpPort", port),
+ "test",
+ "gravitino-host"));
+ }
+
+ private static AuxiliaryServiceManager mockAuxServiceManager(boolean
registered) {
+ AuxiliaryServiceManager serviceManager =
mock(AuxiliaryServiceManager.class);
+
when(serviceManager.isAuxServiceRegistered("iceberg-rest")).thenReturn(registered);
+ return serviceManager;
+ }
}
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
index ce10a056e4..5c94c6b4eb 100644
---
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
@@ -20,8 +20,6 @@ package org.apache.gravitino.server.web.rest;
import com.codahale.metrics.annotation.ResponseMetered;
import com.codahale.metrics.annotation.Timed;
-import java.net.URI;
-import java.net.URISyntaxException;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
@@ -32,7 +30,6 @@ 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.GravitinoEnv;
import org.apache.gravitino.auxiliary.AuxiliaryServiceManager;
import org.apache.gravitino.dto.responses.IcebergRESTServiceResponse;
@@ -52,32 +49,7 @@ public class IcebergRESTServiceOperations {
private static final Logger LOG =
LoggerFactory.getLogger(IcebergRESTServiceOperations.class);
- // Matches gravitino.auxService.names / AuxiliaryServiceManager's
registration key.
private static final String AUX_SERVICE_NAME = "iceberg-rest";
- // Keys below are read from AuxiliaryServiceManager.getAuxServiceConfig,
which already strips
- // the gravitino.iceberg-rest. (or deprecated
gravitino.auxService.iceberg-rest.) prefix, so
- // they must NOT be re-prefixed here.
- // The provider name used by the Iceberg REST server itself; see
- // IcebergConstants.ICEBERG_REST_CATALOG_CONFIG_PROVIDER and
DynamicIcebergConfigProvider. The
- // server module cannot depend on iceberg-common/catalog-common, hence the
literal here.
- private static final String CATALOG_CONFIG_PROVIDER_KEY =
"catalog-config-provider";
- private static final String DYNAMIC_CATALOG_CONFIG_PROVIDER_NAME =
"dynamic-config-provider";
- // The post-strip key used by the Iceberg REST server itself; see
- // IcebergConstants.GRAVITINO_METALAKE and DynamicIcebergConfigProvider.
- private static final String SERVED_METALAKE_KEY = "gravitino-metalake";
- // Overrides the listener-derived endpoint; see docs/iceberg-rest-service.md.
- private static final String ADVERTISED_URI_KEY = "advertised-uri";
- private static final String HOST_KEY = "host";
- private static final String HTTP_PORT_KEY = "httpPort";
- private static final String HTTPS_PORT_KEY = "httpsPort";
- private static final String ENABLE_HTTPS_KEY = "enableHttps";
- // Match IcebergConfig.DEFAULT_ICEBERG_REST_SERVICE_HTTP_PORT/HTTPS_PORT:
the server module
- // cannot depend on iceberg-common, and JettyServerConfig's own defaults are
the Gravitino
- // server's (8090/8433), not the Iceberg REST server's — reading raw values
with these
- // defaults avoids silently reporting the wrong port when httpPort is not
set explicitly.
- private static final int DEFAULT_HTTP_PORT = 9001;
- private static final int DEFAULT_HTTPS_PORT = 9433;
- private static final String DEFAULT_HOST = "0.0.0.0";
@Context private HttpServletRequest httpRequest;
@@ -96,14 +68,19 @@ public class IcebergRESTServiceOperations {
public Response getIcebergRestServiceUri(@QueryParam("metalake") String
metalake) {
String uri;
try {
- uri = resolveUri(metalake);
+ uri =
+ Utils.resolveIcebergRestServiceUri(
+ getAuxServiceManager(),
+ getIcebergRestServiceConfig(),
+ metalake,
+ getHttpRequest().getServerName());
} catch (IllegalStateException e) {
// A misconfiguration, re-reported on every discovery poll until fixed;
the message alone
// identifies it, so the stack trace is omitted from both the log and
the response.
LOG.error("Failed to resolve the Iceberg REST service endpoint: {}",
e.getMessage());
return Utils.internalError(e.getMessage());
}
- // The reported host can depend on the caller's own Host header (see
resolveUri), so this
+ // The reported host can depend on the caller's own Host header, so this
// response must never be cached and replayed to a different caller.
return Response.fromResponse(Utils.ok(new IcebergRESTServiceResponse(uri)))
.header("Cache-Control", "no-store")
@@ -127,116 +104,4 @@ public class IcebergRESTServiceOperations {
HttpServletRequest getHttpRequest() {
return httpRequest;
}
-
- private String resolveUri(String metalake) {
- if (!getAuxServiceManager().isAuxServiceRegistered(AUX_SERVICE_NAME)) {
- return null;
- }
-
- Map<String, String> config = getIcebergRestServiceConfig();
- String provider = config.getOrDefault(CATALOG_CONFIG_PROVIDER_KEY, "");
- if (!DYNAMIC_CATALOG_CONFIG_PROVIDER_NAME.equals(provider)) {
- // Only the dynamic catalog config provider maps Iceberg REST catalog
names onto Gravitino
- // catalogs; the default static provider serves statically-declared
catalogs unrelated to
- // Gravitino catalog names, so routing at it would 404 on every request.
- LOG.debug(
- "Iceberg REST service does not use the dynamic catalog config
provider "
- + "(catalog-config-provider={}); not reporting its endpoint for
auto-discovery.",
- provider);
- return null;
- }
-
- String servedMetalake = config.getOrDefault(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.
- LOG.debug(
- "Iceberg REST service serves metalake {}, not the requested metalake
{}; not "
- + "reporting its endpoint for auto-discovery.",
- servedMetalake,
- metalake);
- return null;
- }
-
- String advertisedUri =
StringUtils.trimToNull(config.get(ADVERTISED_URI_KEY));
- if (advertisedUri != null) {
- return checkAdvertisedUri(advertisedUri);
- }
-
- String host = config.getOrDefault(HOST_KEY, DEFAULT_HOST);
- 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 = getHttpRequest().getServerName();
- }
- boolean enableHttps =
Boolean.parseBoolean(config.getOrDefault(ENABLE_HTTPS_KEY, "false"));
- String scheme = enableHttps ? "https" : "http";
- int port =
- parsePort(
- config,
- enableHttps ? HTTPS_PORT_KEY : HTTP_PORT_KEY,
- enableHttps ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT);
- return String.format("%s://%s:%d/iceberg", scheme, bracketIfIPv6(host),
port);
- }
-
- private static String checkAdvertisedUri(String value) {
- boolean valid;
- try {
- URI uri = new URI(value);
- // URI accepts any non-negative integer as a port; -1 means no explicit
port.
- int port = uri.getPort();
- valid =
- StringUtils.equalsAnyIgnoreCase(uri.getScheme(), "http", "https")
- && StringUtils.isNotBlank(uri.getHost())
- && (port == -1 || (port >= 1 && port <= 65535))
- && uri.getQuery() == null
- && uri.getFragment() == null;
- } catch (URISyntaxException e) {
- valid = false;
- }
- if (!valid) {
- throw new IllegalStateException(
- String.format(
- "Invalid Iceberg REST service %s '%s': expected an absolute
http(s) URI with a "
- + "host, a port in 1-65535 if present, and no query or
fragment",
- ADVERTISED_URI_KEY, value));
- }
- return value;
- }
-
- // An IPv6 literal host (e.g. "::1", from an explicit config value or from
- // HttpServletRequest#getServerName()) must be bracketed to form a valid URI
authority;
- // otherwise its colons are parsed as the port separator. A hostname or IPv4
address never
- // contains a colon, so this only ever fires for IPv6.
- private static String bracketIfIPv6(String host) {
- if (host.contains(":") && !host.startsWith("[")) {
- return "[" + host + "]";
- }
- return host;
- }
-
- private static int parsePort(Map<String, String> config, String key, int
defaultPort) {
- String value = config.getOrDefault(key, "");
- if (StringUtils.isBlank(value)) {
- return defaultPort;
- }
- try {
- return Integer.parseInt(value.trim());
- } catch (NumberFormatException e) {
- return defaultPort;
- }
- }
-
- private static boolean isWildcardHost(String host) {
- return StringUtils.isBlank(host)
- || "0.0.0.0".equals(host)
- || "::".equals(host)
- || "[::]".equals(host)
- || "0:0:0:0:0:0:0:0".equals(host);
- }
}