Skip to content

Commit 2a7a86d

Browse files
committed
feat: add support for configurable host ports in Testcontainers setup
- Introduced dynamic and fixed port binding options through `PortBindingOptions`. - Enabled configuration of specific host ports for various services (e.g., HDFS, Kafka, Hive Metastore). - Added utility function `withServicePort` for managing service port mappings in containers. - Updated documentation and examples to reflect new port binding feature. - Included default `log4j2.xml` configuration for enhanced logging in Spark-related tests.
1 parent e9503a1 commit 2a7a86d

8 files changed

Lines changed: 111 additions & 9 deletions

File tree

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,22 @@ class MyTroubleshootingTest
8989

9090
`ContainerLogMode.STDOUT` prefixes each line with the service name. `ContainerLogMode.FILE` writes files such as `kafka.log`, `schema-registry.log`, `hive-metastore.log`, and `hive-metastore-postgres.log`.
9191

92+
JUnit host ports are random by default. Leave port fields at `0` for Testcontainers dynamic port mapping, or set a positive value when a local tool needs a stable host port:
93+
94+
```kotlin
95+
@BigDataTest(
96+
hdfs = true,
97+
kafka = true,
98+
hdfsNameNodePort = 18020,
99+
hdfsWebPort = 19870,
100+
kafkaPort = 19092,
101+
)
102+
class MyFixedPortTest
103+
```
104+
105+
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.
106+
107+
92108
## Spring Boot
93109

94110
Add the starter and enable the kit in local-development or test configuration:

core/src/main/kotlin/org/openprojectx/bigdata/test/core/BigDataTestKit.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class BigDataTestKit private constructor(
5252
private var kafka = KafkaOptions()
5353
private var localStackS3 = ObjectStoreOptions()
5454
private var fakeGcs = ObjectStoreOptions(image = "fsouza/fake-gcs-server:1.54")
55+
private var portBindings = PortBindingOptions()
5556
private var containerLogs = ContainerLogOptions()
5657

5758
fun withKerberos(options: KerberosOptions = KerberosOptions(enabled = true)): Builder =
@@ -75,6 +76,9 @@ class BigDataTestKit private constructor(
7576
fun withFakeGcs(options: ObjectStoreOptions = ObjectStoreOptions(enabled = true, image = "fsouza/fake-gcs-server:1.54")): Builder =
7677
apply { fakeGcs = options.copy(enabled = true) }
7778

79+
fun withPortBindings(options: PortBindingOptions): Builder =
80+
apply { portBindings = options }
81+
7882
fun withContainerLogs(options: ContainerLogOptions): Builder =
7983
apply { containerLogs = options }
8084

@@ -94,6 +98,7 @@ class BigDataTestKit private constructor(
9498
kafka = kafka,
9599
localStackS3 = localStackS3,
96100
fakeGcs = fakeGcs,
101+
portBindings = portBindings,
97102
containerLogs = containerLogs,
98103
),
99104
)

core/src/main/kotlin/org/openprojectx/bigdata/test/core/BigDataTestKitOptions.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ data class BigDataTestKitOptions(
88
val kafka: KafkaOptions = KafkaOptions(),
99
val localStackS3: ObjectStoreOptions = ObjectStoreOptions(),
1010
val fakeGcs: ObjectStoreOptions = ObjectStoreOptions(image = "fsouza/fake-gcs-server:1.54"),
11+
val portBindings: PortBindingOptions = PortBindingOptions(),
1112
val containerLogs: ContainerLogOptions = ContainerLogOptions(),
1213
)
1314

@@ -87,6 +88,19 @@ data class ObjectStoreOptions(
8788
)
8889

8990

91+
92+
data class PortBindingOptions(
93+
val kerberosKdc: Int = 0,
94+
val hdfsNameNode: Int = 0,
95+
val hdfsWeb: Int = 0,
96+
val hiveMetastore: Int = 0,
97+
val kafka: Int = 0,
98+
val schemaRegistry: Int = 0,
99+
val kafkaUi: Int = 0,
100+
val localStackS3: Int = 0,
101+
val fakeGcs: Int = 0,
102+
)
103+
90104
enum class ContainerLogMode {
91105
NONE,
92106
STDOUT,

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ internal class BigDataContainerFactory(
7575
val container = GenericBigDataContainer(kerberos.image)
7676
.withNetwork(network)
7777
.withNetworkAliases("kerby-kdc")
78-
.withExposedPorts(88)
78+
.withServicePort(88, options.portBindings.kerberosKdc)
7979
.withFileSystemBind(kerberosDirectory(), "/var/lib/kerby")
8080
.withEnv("KERBY_REALM", kerberos.realm)
8181
.withEnv("KERBY_KDC_HOST", "kerby-kdc")
@@ -111,7 +111,8 @@ internal class BigDataContainerFactory(
111111
val container = GenericBigDataContainer(hdfs.image)
112112
.withNetwork(network)
113113
.withNetworkAliases("hdfs", "hdfs.example.com")
114-
.withExposedPorts(hdfs.nameNodePort, hdfs.webPort)
114+
.withServicePort(hdfs.nameNodePort, options.portBindings.hdfsNameNode)
115+
.withServicePort(hdfs.webPort, options.portBindings.hdfsWeb)
115116
.withCommand("sh", "-lc", hdfsStartupCommand(hdfs.nameNodePort, hdfs.webPort))
116117
.waitingFor(Wait.forHttp("/").forPort(hdfs.webPort).withStartupTimeout(Duration.ofMinutes(3)))
117118
if (hdfs.kerberos.enabled) {
@@ -156,7 +157,7 @@ internal class BigDataContainerFactory(
156157
val container = GenericBigDataContainer(hive.image)
157158
.withNetwork(network)
158159
.withNetworkAliases("hive-metastore", "hive-metastore.example.com")
159-
.withExposedPorts(9083)
160+
.withServicePort(9083, options.portBindings.hiveMetastore)
160161
.withEnv("POSTGRES_DB", hive.databaseName)
161162
.withEnv("POSTGRES_USER", hive.databaseUser)
162163
.withEnv("POSTGRES_PASSWORD", hive.databasePassword)
@@ -199,7 +200,7 @@ internal class BigDataContainerFactory(
199200
val container = GenericBigDataContainer(kafka.image)
200201
.withNetwork(network)
201202
.withNetworkAliases("kafka", "broker1.example.com")
202-
.withExposedPorts(9092)
203+
.withServicePort(9092, options.portBindings.kafka)
203204
.withEnv("KAFKA_NODE_ID", "1")
204205
.withEnv("KAFKA_PROCESS_ROLES", "broker,controller")
205206
.withEnv("KAFKA_CONTROLLER_QUORUM_VOTERS", "1@kafka:29093")
@@ -251,7 +252,7 @@ internal class BigDataContainerFactory(
251252
val container = GenericBigDataContainer(kafka.schemaRegistryImage)
252253
.withNetwork(network)
253254
.withNetworkAliases("schema-registry")
254-
.withExposedPorts(8085)
255+
.withServicePort(8085, options.portBindings.schemaRegistry)
255256
.withEnv("SCHEMA_REGISTRY_HOST_NAME", "schema-registry")
256257
.withEnv("SCHEMA_REGISTRY_LISTENERS", "http://0.0.0.0:8085")
257258
.withEnv("SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS", "PLAINTEXT://kafka:9092")
@@ -286,7 +287,7 @@ internal class BigDataContainerFactory(
286287
val container = GenericBigDataContainer(kafka.kafkaUiImage)
287288
.withNetwork(network)
288289
.withNetworkAliases("kafka-ui")
289-
.withExposedPorts(8080)
290+
.withServicePort(8080, options.portBindings.kafkaUi)
290291
.withEnv("KAFKA_CLUSTERS_0_NAME", "local")
291292
.withEnv("KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS", "kafka:9092")
292293
.withEnv("DYNAMIC_CONFIG_ENABLED", "false")
@@ -318,7 +319,7 @@ internal class BigDataContainerFactory(
318319
val container = GenericBigDataContainer(objectStore.image)
319320
.withNetwork(network)
320321
.withNetworkAliases("localstack")
321-
.withExposedPorts(4566)
322+
.withServicePort(4566, options.portBindings.localStackS3)
322323
.withEnv("SERVICES", "s3")
323324
.waitingFor(Wait.forHttp("/_localstack/health").forStatusCode(200).withStartupTimeout(Duration.ofMinutes(3)))
324325

@@ -344,7 +345,7 @@ internal class BigDataContainerFactory(
344345
val container = GenericBigDataContainer(objectStore.image)
345346
.withNetwork(network)
346347
.withNetworkAliases("fake-gcs")
347-
.withExposedPorts(4443)
348+
.withServicePort(4443, options.portBindings.fakeGcs)
348349
.withCommand("-scheme", "http", "-port", "4443")
349350
.waitingFor(Wait.forHttp("/storage/v1/b").forStatusCode(200).withStartupTimeout(Duration.ofMinutes(2)))
350351

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
package org.openprojectx.bigdata.test.core.container
22

33
import org.testcontainers.containers.GenericContainer
4+
import org.testcontainers.containers.InternetProtocol
45
import org.testcontainers.utility.DockerImageName
56

67
internal class GenericBigDataContainer(image: String) :
7-
GenericContainer<GenericBigDataContainer>(DockerImageName.parse(image))
8+
GenericContainer<GenericBigDataContainer>(DockerImageName.parse(image)) {
9+
fun withServicePort(containerPort: Int, hostPort: Int = 0): GenericBigDataContainer =
10+
apply {
11+
require(hostPort >= 0) { "Host port must be 0 for random binding or a positive fixed port" }
12+
if (hostPort == 0) {
13+
addExposedPort(containerPort)
14+
} else {
15+
addFixedExposedPort(hostPort, containerPort, InternetProtocol.TCP)
16+
}
17+
}
18+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Configuration status="WARN" monitorInterval="30">
3+
<Properties>
4+
<Property name="CONSOLE_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight{%-5level}{FATAL=red, ERROR=red, WARN=yellow, INFO=green, DEBUG=cyan, TRACE=blue} [%style{%20.20t}{magenta}] %style{%logger}{cyan} - %msg%n%throwable</Property>
5+
</Properties>
6+
7+
<Appenders>
8+
<Console name="Console" target="SYSTEM_OUT">
9+
<PatternLayout pattern="${CONSOLE_PATTERN}" disableAnsi="false"/>
10+
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
11+
</Console>
12+
</Appenders>
13+
14+
<Loggers>
15+
<Logger name="org.openprojectx" level="DEBUG" additivity="false">
16+
<AppenderRef ref="Console"/>
17+
</Logger>
18+
19+
<!-- <Logger name="org.apache.spark.scheduler" level="DEBUG"/>-->
20+
<!-- <Logger name="org.apache.spark.scheduler.TaskSchedulerImpl" level="DEBUG"/>-->
21+
<!-- <Logger name="org.apache.spark.ExecutorAllocationManager" level="DEBUG"/>-->
22+
<!-- <Logger name="org.apache.spark.deploy.k8s" level="DEBUG"/>-->
23+
<!-- <Logger name="org.apache.spark.scheduler.cluster.k8s" level="DEBUG"/>-->
24+
<!-- <Logger name="org.apache.iceberg" level="DEBUG"/>-->
25+
<!-- <Logger name="org.apache.iceberg.rest" level="DEBUG"/>-->
26+
<!-- <Logger name="org.apache.hadoop.hive" level="DEBUG"/>-->
27+
28+
<Root level="INFO">
29+
<AppenderRef ref="Console"/>
30+
</Root>
31+
</Loggers>
32+
</Configuration>

junit5/src/main/kotlin/org/openprojectx/bigdata/test/junit5/BigDataTest.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ annotation class BigDataTest(
2020
val kafkaUiKerberos: Boolean = false,
2121
val localStackS3: Boolean = false,
2222
val fakeGcs: Boolean = false,
23+
val kerberosKdcPort: Int = 0,
24+
val hdfsNameNodePort: Int = 0,
25+
val hdfsWebPort: Int = 0,
26+
val hiveMetastorePort: Int = 0,
27+
val kafkaPort: Int = 0,
28+
val schemaRegistryPort: Int = 0,
29+
val kafkaUiPort: Int = 0,
30+
val localStackS3Port: Int = 0,
31+
val fakeGcsPort: Int = 0,
2332
val containerLogMode: ContainerLogMode = ContainerLogMode.NONE,
2433
val containerLogDirectory: String = "build/bigdata-test-container-logs",
2534
)

junit5/src/main/kotlin/org/openprojectx/bigdata/test/junit5/BigDataTestExtension.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import org.openprojectx.bigdata.test.core.ContainerLogMode
1212
import org.openprojectx.bigdata.test.core.ContainerLogOptions
1313
import org.openprojectx.bigdata.test.core.KafkaOptions
1414
import org.openprojectx.bigdata.test.core.KerberosAuthOptions
15+
import org.openprojectx.bigdata.test.core.PortBindingOptions
1516

1617
class BigDataTestExtension : BeforeAllCallback, AfterAllCallback, ParameterResolver {
1718
override fun beforeAll(context: ExtensionContext) {
@@ -38,6 +39,19 @@ class BigDataTestExtension : BeforeAllCallback, AfterAllCallback, ParameterResol
3839

3940
private fun kitFrom(annotation: BigDataTest): BigDataTestKit {
4041
val builder = BigDataTestKit.builder()
42+
.withPortBindings(
43+
PortBindingOptions(
44+
kerberosKdc = annotation.kerberosKdcPort,
45+
hdfsNameNode = annotation.hdfsNameNodePort,
46+
hdfsWeb = annotation.hdfsWebPort,
47+
hiveMetastore = annotation.hiveMetastorePort,
48+
kafka = annotation.kafkaPort,
49+
schemaRegistry = annotation.schemaRegistryPort,
50+
kafkaUi = annotation.kafkaUiPort,
51+
localStackS3 = annotation.localStackS3Port,
52+
fakeGcs = annotation.fakeGcsPort,
53+
),
54+
)
4155
if (annotation.containerLogMode != ContainerLogMode.NONE) {
4256
builder.withContainerLogs(
4357
ContainerLogOptions(

0 commit comments

Comments
 (0)