Skip to content

Commit adc0bc2

Browse files
Dmitriy FingermanDmitriy Fingerman
authored andcommitted
HIVE-29468: Standalone HMS REST Catalog Server: Migrate to Spring Boot
1 parent 89631dc commit adc0bc2

14 files changed

Lines changed: 969 additions & 261 deletions

File tree

itests/hive-iceberg/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@
5151
<version>${keycloak.version}</version>
5252
<scope>test</scope>
5353
</dependency>
54+
<dependency>
55+
<groupId>jakarta.annotation</groupId>
56+
<artifactId>jakarta.annotation-api</artifactId>
57+
<version>${jakarta.annotation.version}</version>
58+
<scope>test</scope>
59+
</dependency>
5460
<dependency>
5561
<groupId>org.apache.hive</groupId>
5662
<artifactId>hive-standalone-metastore-common</artifactId>

itests/qtest-iceberg/pom.xml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,27 @@
475475
<version>${project.version}</version>
476476
<scope>test</scope>
477477
</dependency>
478+
<!-- Spring Boot test dependencies -->
479+
<dependency>
480+
<groupId>org.springframework.boot</groupId>
481+
<artifactId>spring-boot-starter-test</artifactId>
482+
<version>${spring-boot.version}</version>
483+
<scope>test</scope>
484+
<exclusions>
485+
<exclusion>
486+
<groupId>org.springframework.boot</groupId>
487+
<artifactId>spring-boot-starter-logging</artifactId>
488+
</exclusion>
489+
<exclusion>
490+
<groupId>org.junit.jupiter</groupId>
491+
<artifactId>junit-jupiter</artifactId>
492+
</exclusion>
493+
<exclusion>
494+
<groupId>org.junit.vintage</groupId>
495+
<artifactId>junit-vintage-engine</artifactId>
496+
</exclusion>
497+
</exclusions>
498+
</dependency>
478499
<dependency>
479500
<groupId>org.testcontainers</groupId>
480501
<artifactId>testcontainers</artifactId>
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hadoop.hive.cli;
19+
20+
import java.io.File;
21+
import java.io.IOException;
22+
import java.util.UUID;
23+
24+
import org.apache.hadoop.conf.Configuration;
25+
import org.apache.hadoop.hive.common.FileUtils;
26+
import org.apache.http.HttpHeaders;
27+
import org.apache.http.client.methods.CloseableHttpResponse;
28+
import org.apache.http.client.methods.HttpGet;
29+
import org.apache.http.client.methods.HttpPost;
30+
import org.apache.http.entity.StringEntity;
31+
import org.apache.http.impl.client.CloseableHttpClient;
32+
import org.apache.http.impl.client.HttpClients;
33+
import org.apache.http.util.EntityUtils;
34+
import org.apache.hadoop.hive.metastore.MetaStoreTestUtils;
35+
import org.apache.hadoop.hive.metastore.security.HadoopThriftAuthBridge;
36+
import org.apache.hadoop.hive.metastore.conf.MetastoreConf;
37+
import org.apache.hadoop.hive.metastore.conf.MetastoreConf.ConfVars;
38+
import org.apache.iceberg.rest.standalone.IcebergCatalogConfiguration;
39+
import org.apache.iceberg.rest.standalone.StandaloneRESTCatalogServer;
40+
import org.slf4j.Logger;
41+
import org.slf4j.LoggerFactory;
42+
import org.springframework.boot.autoconfigure.SpringBootApplication;
43+
import org.springframework.context.annotation.Import;
44+
import org.springframework.core.Ordered;
45+
import org.springframework.core.annotation.Order;
46+
import org.springframework.test.context.TestContext;
47+
import org.springframework.test.context.TestExecutionListener;
48+
49+
import static org.junit.Assert.assertEquals;
50+
import static org.junit.Assert.assertNotNull;
51+
import static org.junit.Assert.assertTrue;
52+
53+
/**
54+
* Base class for Standalone REST Catalog Server integration tests.
55+
*
56+
* Provides shared setup (HMS, listeners), HTTP helpers (with optional auth), and common tests
57+
* (liveness, readiness, Prometheus, server port). Subclasses provide auth-specific configuration
58+
* and tests.
59+
*/
60+
public abstract class BaseStandaloneRESTCatalogServerTest {
61+
protected static final Logger LOG = LoggerFactory.getLogger(BaseStandaloneRESTCatalogServerTest.class);
62+
private static final String REST_CATALOG_URL_TEMPLATE = "http://localhost:%d%s";
63+
64+
protected static Configuration hmsConf;
65+
protected static int hmsPort;
66+
protected static File warehouseDir;
67+
protected static File hmsTempDir;
68+
69+
/**
70+
* Starts HMS before the Spring ApplicationContext loads.
71+
* Spring loads the context before @BeforeClass, so we use a TestExecutionListener
72+
* which runs before context initialization.
73+
*/
74+
@Order(Ordered.HIGHEST_PRECEDENCE)
75+
public static class HmsStartupListener implements TestExecutionListener {
76+
private static final String TEMP_DIR_PREFIX = "StandaloneRESTCatalogServer";
77+
78+
@Override
79+
public void beforeTestClass(TestContext testContext) throws Exception {
80+
if (hmsPort > 0) {
81+
return;
82+
}
83+
String uniqueTestKey = String.format("%s_%s", TEMP_DIR_PREFIX, UUID.randomUUID());
84+
hmsTempDir = new File(MetaStoreTestUtils.getTestWarehouseDir(uniqueTestKey));
85+
hmsTempDir.mkdirs();
86+
warehouseDir = new File(hmsTempDir, "warehouse");
87+
warehouseDir.mkdirs();
88+
89+
hmsConf = MetastoreConf.newMetastoreConf();
90+
MetaStoreTestUtils.setConfForStandloneMode(hmsConf);
91+
92+
String jdbcUrl = String.format("jdbc:derby:memory:%s;create=true",
93+
new File(hmsTempDir, "metastore_db").getAbsolutePath());
94+
MetastoreConf.setVar(hmsConf, ConfVars.CONNECT_URL_KEY, jdbcUrl);
95+
MetastoreConf.setVar(hmsConf, ConfVars.WAREHOUSE, warehouseDir.getAbsolutePath());
96+
MetastoreConf.setVar(hmsConf, ConfVars.WAREHOUSE_EXTERNAL, warehouseDir.getAbsolutePath());
97+
98+
hmsPort = MetaStoreTestUtils.startMetaStoreWithRetry(
99+
HadoopThriftAuthBridge.getBridge(), hmsConf, true, false, false, false);
100+
LOG.info("Started embedded HMS on port: {} (before Spring context)", hmsPort);
101+
}
102+
}
103+
104+
@SpringBootApplication
105+
@Import(IcebergCatalogConfiguration.class)
106+
public static class TestRestCatalogApplication {}
107+
108+
protected String url(String path) {
109+
return String.format(REST_CATALOG_URL_TEMPLATE, getPort(), path);
110+
}
111+
112+
/**
113+
* Returns the server port. Subclasses must provide this (e.g. from @LocalServerPort).
114+
*/
115+
protected abstract int getPort();
116+
117+
/**
118+
* Creates a GET request with optional Bearer token.
119+
*
120+
* @param path the request path (e.g. "/iceberg/v1/config")
121+
* @param bearerToken optional Bearer token; if null, no Authorization header is set
122+
*/
123+
protected HttpGet get(String path, String bearerToken) {
124+
HttpGet request = new HttpGet(url(path));
125+
request.setHeader("Content-Type", "application/json");
126+
if (bearerToken != null) {
127+
request.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + bearerToken);
128+
}
129+
return request;
130+
}
131+
132+
/**
133+
* Creates a GET request without auth.
134+
*/
135+
protected HttpGet get(String path) {
136+
return get(path, null);
137+
}
138+
139+
/**
140+
* Creates a POST request with optional Bearer token.
141+
*
142+
* @param path the request path
143+
* @param jsonBody the JSON body
144+
* @param bearerToken optional Bearer token; if null, no Authorization header is set
145+
*/
146+
protected HttpPost post(String path, String jsonBody, String bearerToken) {
147+
HttpPost request = new HttpPost(url(path));
148+
request.setHeader("Content-Type", "application/json");
149+
if (bearerToken != null) {
150+
request.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + bearerToken);
151+
}
152+
if (jsonBody != null) {
153+
request.setEntity(new StringEntity(jsonBody, "UTF-8"));
154+
}
155+
return request;
156+
}
157+
158+
/**
159+
* Creates a POST request without auth.
160+
*/
161+
protected HttpPost post(String path, String jsonBody) {
162+
return post(path, jsonBody, null);
163+
}
164+
165+
protected static void teardownBase() throws IOException {
166+
if (hmsPort > 0) {
167+
MetaStoreTestUtils.close(hmsPort);
168+
}
169+
if (hmsTempDir != null && hmsTempDir.exists()) {
170+
FileUtils.deleteDirectory(hmsTempDir);
171+
}
172+
}
173+
174+
protected void testLivenessProbe() throws Exception {
175+
try (CloseableHttpClient httpClient = HttpClients.createDefault();
176+
CloseableHttpResponse response = httpClient.execute(get("/actuator/health/liveness"))) {
177+
assertEquals("Liveness probe should return 200", 200, response.getStatusLine().getStatusCode());
178+
String body = EntityUtils.toString(response.getEntity());
179+
assertTrue("Liveness should be UP", body.contains("UP"));
180+
LOG.info("Liveness probe passed: {}", body);
181+
}
182+
}
183+
184+
protected void testReadinessProbe() throws Exception {
185+
try (CloseableHttpClient httpClient = HttpClients.createDefault();
186+
CloseableHttpResponse response = httpClient.execute(get("/actuator/health/readiness"))) {
187+
assertEquals("Readiness probe should return 200", 200, response.getStatusLine().getStatusCode());
188+
String body = EntityUtils.toString(response.getEntity());
189+
assertTrue("Readiness should be UP", body.contains("UP"));
190+
LOG.info("Readiness probe passed: {}", body);
191+
}
192+
}
193+
194+
protected void testPrometheusMetrics() throws Exception {
195+
try (CloseableHttpClient httpClient = HttpClients.createDefault();
196+
CloseableHttpResponse response = httpClient.execute(get("/actuator/prometheus"))) {
197+
assertEquals("Metrics endpoint should return 200", 200, response.getStatusLine().getStatusCode());
198+
String body = EntityUtils.toString(response.getEntity());
199+
assertTrue("Should contain JVM metrics", body.contains("jvm_memory"));
200+
LOG.info("Prometheus metrics available");
201+
}
202+
}
203+
204+
protected void testServerPort(StandaloneRESTCatalogServer server) {
205+
assertTrue("Server port should be > 0", getPort() > 0);
206+
assertNotNull("REST endpoint should not be null", server.getRestEndpoint());
207+
LOG.info("Server port: {}, Endpoint: {}", getPort(), server.getRestEndpoint());
208+
}
209+
}

0 commit comments

Comments
 (0)