Skip to content

Commit 2e5df2d

Browse files
committed
feat: improve HMS object store integration and external table validation
- Injected Hadoop filesystem configuration for HMS when using LocalStack S3 or fake GCS. - Updated Spark example to validate Hive external Parquet tables with enhanced metadata checks. - Refactored container initialization to ensure consistent order of service startup. - Added support for validating table location and input format in HMS metadata verification.
1 parent 972074e commit 2e5df2d

3 files changed

Lines changed: 52 additions & 17 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ class MyFixedPortTest
159159

160160
Available JUnit port fields are `kerberosKdcPort`, `hdfsNameNodePort`, `hdfsWebPort`, `hiveMetastorePort`, `kafkaPort`, `schemaRegistryPort`, `kafkaUiPort`, `localStackS3Port`, and `fakeGcsPort`. Endpoint properties still use the actual mapped host ports returned by Testcontainers.
161161

162+
When Hive Metastore is started with LocalStack S3 or fake GCS, the kit also injects server-side Hadoop filesystem configuration into HMS. S3 external table DDL can validate `s3a://` locations against the internal LocalStack endpoint. GCS config is also injected, but HMS still needs a GCS connector jar in the HMS image before it can validate `gs://` locations.
162163

163164
## Spring Boot
164165

@@ -203,7 +204,7 @@ GRADLE_USER_HOME=/data/.gradle ./gradlew :example:spring:bootRun --args='--sprin
203204

204205
The JUnit examples in `example/junit` are annotated with `@Disabled`; remove that annotation from an example class to start the configured stack.
205206

206-
The Spark example in `example/spark` creates a `SparkSession` from `BigDataTestKit` endpoints, starts HDFS, Hive Metastore, Kafka, Schema Registry, LocalStack S3, and fake GCS, then uses `extensions` config to create the S3 JCEKS file on HDFS and produce Avro records to Kafka. HDFS is used as the config store for the JCEKS file; S3 and GCS are object-store Iceberg smoke checks through Hadoop catalogs. The example also creates an HMS-backed Iceberg table and a Hive metastore Parquet table stored on S3, then verifies the HMS database/table metadata through `HiveMetaStoreClient`.
207+
The Spark example in `example/spark` creates a `SparkSession` from `BigDataTestKit` endpoints, starts HDFS, Hive Metastore, Kafka, Schema Registry, LocalStack S3, and fake GCS, then uses `extensions` config to create the S3 JCEKS file on HDFS and produce Avro records to Kafka. HDFS is used as the config store for the JCEKS file; S3 and GCS are object-store Iceberg smoke checks through Hadoop catalogs. The example also creates an HMS-backed Iceberg table and a Hive external Parquet table stored on S3, then verifies the HMS database/table location and format metadata through `HiveMetaStoreClient`.
207208

208209
```bash
209210
GRADLE_USER_HOME=/data/.gradle ./gradlew :example:spark:test

core/src/main/kotlin/org/openprojectx/bigdata/test/core/container/BigDataContainerFactory.kt

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@ internal class BigDataContainerFactory(
3737
val containers = mutableListOf<BigDataServiceContainer>()
3838
if (kerberosRequired()) containers += kerberos()
3939
if (options.hdfs.enabled) containers += hdfs()
40+
if (options.localStackS3.enabled) containers += localStackS3()
41+
if (options.fakeGcs.enabled) containers += fakeGcs()
4042
if (options.hiveMetastore.enabled) containers += hiveMetastore()
4143
if (options.kafka.enabled) {
4244
containers += kafka()
4345
if (options.kafka.schemaRegistryEnabled) containers += schemaRegistry()
4446
if (options.kafka.kafkaUiEnabled) containers += kafkaUi()
4547
}
46-
if (options.localStackS3.enabled) containers += localStackS3()
47-
if (options.fakeGcs.enabled) containers += fakeGcs()
4848
return containers
4949
}
5050

@@ -179,6 +179,9 @@ internal class BigDataContainerFactory(
179179
.withEnv("HMS_CONF_HADOOP_SECURITY_AUTHENTICATION", "kerberos")
180180
}
181181

182+
hiveMetastoreObjectStoreConfiguration().forEach { (key, value) ->
183+
container.withEnv("HMS_CONF_${encodeConfigKey(key)}", value)
184+
}
182185
hive.extraConfiguration.forEach { (key, value) ->
183186
container.withEnv("HMS_CONF_${encodeConfigKey(key)}", value)
184187
}
@@ -388,6 +391,33 @@ internal class BigDataContainerFactory(
388391
}
389392

390393

394+
private fun hiveMetastoreObjectStoreConfiguration(): Map<String, String> =
395+
buildMap {
396+
if (options.localStackS3.enabled) {
397+
put("fs.s3a.endpoint", "http://localstack:4566")
398+
put("fs.s3a.endpoint.region", "us-east-1")
399+
put("fs.s3a.aws.credentials.provider", "org.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider")
400+
put("fs.s3a.access.key", "test")
401+
put("fs.s3a.secret.key", "test")
402+
put("fs.s3a.path.style.access", "true")
403+
put("fs.s3a.connection.ssl.enabled", "false")
404+
put("fs.s3a.change.detection.mode", "none")
405+
}
406+
if (options.fakeGcs.enabled) {
407+
put("fs.gs.impl", "com.google.cloud.hadoop.fs.gcs.GoogleHadoopFileSystem")
408+
put("fs.AbstractFileSystem.gs.impl", "com.google.cloud.hadoop.fs.gcs.GoogleHadoopFS")
409+
put("fs.gs.project.id", "bigdata-test")
410+
put("fs.gs.storage.root.url", "http://fake-gcs:4443/")
411+
put("fs.gs.storage.service.path", "storage/v1/")
412+
put("fs.gs.client.type", "HTTP_API_CLIENT")
413+
put("fs.gs.auth.type", "UNAUTHENTICATED")
414+
put("fs.gs.create.items.conflict.check.enable", "false")
415+
put("fs.gs.implicit.dir.repair.enable", "false")
416+
put("fs.gs.hierarchical.namespace.folders.enable", "false")
417+
}
418+
}
419+
420+
391421
private fun <T : GenericContainer<*>> attachLogs(name: String, container: T): T {
392422
when (options.containerLogs.mode) {
393423
ContainerLogMode.NONE -> Unit

example/spark/src/test/kotlin/org/openprojectx/bigdata/test/example/spark/SparkBigDataTestExample.kt

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,13 @@ class SparkBigDataTestExample {
7373
database = "hms_demo_$runId",
7474
table = "events_hms",
7575
)
76-
assertHiveS3ParquetTable(
76+
assertHiveExternalParquetTable(
7777
spark = spark,
7878
hiveMetastoreUri = hiveMetastore.property("hive.metastore.uris"),
7979
database = "hive_s3_demo_$runId",
8080
table = "events_parquet_s3",
8181
location = "s3a://$s3Bucket/hive-parquet/events_parquet_s3",
82+
storageName = "hive-s3-parquet",
8283
)
8384
}
8485
}
@@ -195,49 +196,52 @@ class SparkBigDataTestExample {
195196
}
196197
}
197198

198-
private fun assertHiveS3ParquetTable(
199+
private fun assertHiveExternalParquetTable(
199200
spark: SparkSession,
200201
hiveMetastoreUri: String,
201202
database: String,
202203
table: String,
203204
location: String,
205+
storageName: String,
204206
) {
205207
val identifier = "$database.$table"
206208
spark.sql("CREATE DATABASE IF NOT EXISTS $database")
207209
spark.sql(
208210
"""
209-
CREATE TABLE $identifier (
211+
CREATE EXTERNAL TABLE $identifier (
210212
id INT,
211213
name STRING,
212214
storage STRING
213-
) USING PARQUET
215+
) STORED AS PARQUET
214216
LOCATION '$location'
215217
""".trimIndent(),
216218
)
217-
spark.sql("INSERT INTO $identifier VALUES (1, 'alpha', 'hive-s3-parquet'), (2, 'beta', 'hive-s3-parquet')")
218-
val count = spark.table(identifier).where("storage = 'hive-s3-parquet'").count()
219-
check(count == 2L) { "Expected two Hive S3 Parquet rows in $identifier" }
220-
assertS3ParquetFiles(spark, location)
221-
assertHiveMetastoreParquetS3Table(hiveMetastoreUri, database, table)
219+
spark.sql("INSERT INTO $identifier VALUES (1, 'alpha', '$storageName'), (2, 'beta', '$storageName')")
220+
val count = spark.table(identifier).where("storage = '$storageName'").count()
221+
check(count == 2L) { "Expected two Hive external Parquet rows in $identifier" }
222+
assertParquetFiles(spark, location)
223+
assertHiveMetastoreExternalParquetTable(hiveMetastoreUri, database, table, location)
222224
}
223225

224-
private fun assertS3ParquetFiles(spark: SparkSession, location: String) {
226+
private fun assertParquetFiles(spark: SparkSession, location: String) {
225227
val files = org.apache.hadoop.fs.FileSystem.get(URI.create(location), spark.sparkContext().hadoopConfiguration())
226228
.use { fs -> fs.listStatus(org.apache.hadoop.fs.Path(location)).map { it.path.name } }
227229
check(files.any { it.endsWith(".parquet") }) { "Expected Parquet files under $location, got $files" }
228230
}
229231

230-
private fun assertHiveMetastoreParquetS3Table(hiveMetastoreUri: String, database: String, table: String) {
232+
private fun assertHiveMetastoreExternalParquetTable(hiveMetastoreUri: String, database: String, table: String, location: String) {
231233
val conf = HiveConf()
232234
conf.setVar(HiveConf.ConfVars.METASTOREURIS, hiveMetastoreUri)
233235
val client = HiveMetaStoreClient(conf)
234236
try {
235237
val hmsTable = client.getTable(database, table)
236238
check(hmsTable.dbName == database) { "Expected HMS table $database.$table, got ${hmsTable.dbName}.${hmsTable.tableName}" }
237239
check(hmsTable.tableName == table) { "Expected HMS table $database.$table, got ${hmsTable.dbName}.${hmsTable.tableName}" }
238-
val provider = hmsTable.parameters["spark.sql.sources.provider"] ?: hmsTable.parameters["provider"]
239-
check(provider.equals("parquet", ignoreCase = true) || hmsTable.sd.inputFormat.contains("Parquet")) {
240-
"Expected HMS table $database.$table to be Parquet, parameters=${hmsTable.parameters}, inputFormat=${hmsTable.sd.inputFormat}"
240+
check(hmsTable.sd.location == location) {
241+
"Expected HMS table $database.$table location $location, got ${hmsTable.sd.location}"
242+
}
243+
check(hmsTable.sd.inputFormat.contains("Parquet")) {
244+
"Expected HMS table $database.$table to be Parquet, inputFormat=${hmsTable.sd.inputFormat}"
241245
}
242246
} finally {
243247
client.close()

0 commit comments

Comments
 (0)