Skip to content

Commit 8caf92f

Browse files
committed
feat: add HDFS DataNode customization and CLI health checks
- Introduced `hdfsDataNodeHostname` and `hdfsDataNodePort` options for fine-tuned HDFS configuration. - Added support for container-level customizations, such as environment variables, file copies, ports, and mounts via programmatic and TOML configurations. - Enabled optional CLI-based health checks for all services, including HDFS, Kafka, and Hive Metastore. - Extended documentation with detailed examples and guidance for DataNode configuration, container customizations, and health checks. - Updated Spark example with additional Jackson version handling for Cloudera dependencies.
1 parent 663d9cf commit 8caf92f

10 files changed

Lines changed: 710 additions & 23 deletions

File tree

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,26 @@ domain = "localhost"
7878

7979
The endpoint properties then return HTTPS URLs and JVM truststore settings such as `javax.net.ssl.trustStore`.
8080

81+
For image-specific troubleshooting, containers can be customized from TOML:
82+
83+
```toml
84+
[containers.hdfs.env]
85+
HADOOP_OPTS = "-Dsun.security.krb5.debug=true"
86+
87+
[containers.hdfs.files]
88+
"/tmp/message.txt" = "text:created by bigdata-test"
89+
```
90+
91+
Programmatic tests can also call `customizeContainer(...)` for last-resort Testcontainers access.
92+
93+
Optional CLI health checks can run after container startup:
94+
95+
```toml
96+
[healthChecks]
97+
hdfs = "cli"
98+
localStackS3 = "cli"
99+
```
100+
81101
## Run Examples
82102

83103
Use the shared Gradle home when running this repository locally:

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ enum class BigDataService(
1717
),
1818
),
1919
HDFS(
20-
defaultPorts = mapOf("namenode" to 8020, "web" to 9870, "web-tls" to 443),
21-
endpointProperties = setOf("fs.defaultFS", "spring.hadoop.fs-uri", "dfs.namenode.https-address"),
20+
defaultPorts = mapOf("namenode" to 8020, "datanode" to 9866, "web" to 9870, "web-tls" to 443),
21+
endpointProperties = setOf(
22+
"fs.defaultFS",
23+
"spring.hadoop.fs-uri",
24+
"dfs.client.use.datanode.hostname",
25+
"dfs.datanode.hostname",
26+
"dfs.namenode.https-address",
27+
),
2228
),
2329
HIVE_METASTORE(
2430
defaultPorts = mapOf("thrift" to 9083),

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

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

33
import org.openprojectx.bigdata.test.core.container.BigDataContainerFactory
4+
import org.testcontainers.containers.GenericContainer
45
import org.testcontainers.lifecycle.Startable
6+
import java.util.function.Consumer
57

68
class BigDataTestKit private constructor(
79
private val options: BigDataTestKitOptions,
@@ -16,7 +18,9 @@ class BigDataTestKit private constructor(
1618
serviceContainers.forEach {
1719
it.container.start()
1820
it.afterStart()
19-
endpoints[it.service] = it.endpoint()
21+
val endpoint = it.endpoint()
22+
endpoints[it.service] = endpoint
23+
factory.healthCheck(it.service, it.container, endpoint)
2024
}
2125
started = true
2226
}
@@ -57,6 +61,8 @@ class BigDataTestKit private constructor(
5761
private var fakeGcs = ObjectStoreOptions(image = "fsouza/fake-gcs-server:1.54")
5862
private var portBindings = PortBindingOptions()
5963
private var containerLogs = ContainerLogOptions()
64+
private var containerCustomizations = emptyMap<BigDataService, ContainerCustomizationOptions>()
65+
private var healthChecks = emptyMap<BigDataService, BigDataHealthCheckOptions>()
6066

6167
fun withKerberos(options: KerberosOptions = KerberosOptions(enabled = true)): Builder =
6268
apply { kerberos = options.copy(enabled = true) }
@@ -103,6 +109,43 @@ class BigDataTestKit private constructor(
103109
fun withContainerLogsToDirectory(directory: String = "build/bigdata-test-container-logs"): Builder =
104110
withContainerLogs(ContainerLogOptions(mode = ContainerLogMode.FILE, directory = directory))
105111

112+
fun withContainerCustomization(service: BigDataService, options: ContainerCustomizationOptions): Builder =
113+
apply { containerCustomizations = containerCustomizations.merge(service, options) }
114+
115+
fun withContainerEnv(service: BigDataService, name: String, value: String): Builder =
116+
withContainerCustomization(
117+
service,
118+
ContainerCustomizationOptions(environment = mapOf(name to value)),
119+
)
120+
121+
fun withContainerFile(service: BigDataService, file: ContainerFileTransferOptions): Builder =
122+
withContainerCustomization(service, ContainerCustomizationOptions(files = listOf(file)))
123+
124+
fun withContainerMount(service: BigDataService, mount: ContainerMountOptions): Builder =
125+
withContainerCustomization(service, ContainerCustomizationOptions(mounts = listOf(mount)))
126+
127+
fun withContainerPort(service: BigDataService, port: ContainerPortOptions): Builder =
128+
withContainerCustomization(service, ContainerCustomizationOptions(ports = listOf(port)))
129+
130+
fun customizeContainer(service: BigDataService, customizer: BigDataContainerCustomizer): Builder =
131+
withContainerCustomization(service, ContainerCustomizationOptions(customizers = listOf(customizer)))
132+
133+
fun customizeContainer(service: BigDataService, customizer: Consumer<GenericContainer<*>>): Builder =
134+
customizeContainer(service, BigDataContainerCustomizer { customizer.accept(it) })
135+
136+
fun withHealthCheck(service: BigDataService, options: BigDataHealthCheckOptions): Builder =
137+
apply { healthChecks = healthChecks + (service to options) }
138+
139+
fun withCliHealthCheck(service: BigDataService): Builder =
140+
withHealthCheck(service, BigDataHealthCheckOptions(mode = BigDataHealthCheckMode.CLI))
141+
142+
fun withCliHealthChecks(): Builder =
143+
apply {
144+
BigDataService.entries.forEach {
145+
withCliHealthCheck(it)
146+
}
147+
}
148+
106149
fun build(): BigDataTestKit =
107150
BigDataTestKit(
108151
BigDataTestKitOptions(
@@ -115,7 +158,15 @@ class BigDataTestKit private constructor(
115158
fakeGcs = fakeGcs,
116159
portBindings = portBindings,
117160
containerLogs = containerLogs,
161+
containerCustomizations = containerCustomizations,
162+
healthChecks = healthChecks,
118163
),
119164
)
165+
166+
private fun Map<BigDataService, ContainerCustomizationOptions>.merge(
167+
service: BigDataService,
168+
options: ContainerCustomizationOptions,
169+
): Map<BigDataService, ContainerCustomizationOptions> =
170+
this + (service to (this[service]?.merge(options) ?: options))
120171
}
121172
}

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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.openprojectx.bigdata.test.core
22

3+
import org.testcontainers.containers.GenericContainer
4+
35
data class BigDataTestKitOptions(
46
val kerberos: KerberosOptions = KerberosOptions(),
57
val tls: TlsOptions = TlsOptions(),
@@ -10,6 +12,8 @@ data class BigDataTestKitOptions(
1012
val fakeGcs: ObjectStoreOptions = ObjectStoreOptions(image = "fsouza/fake-gcs-server:1.54"),
1113
val portBindings: PortBindingOptions = PortBindingOptions(),
1214
val containerLogs: ContainerLogOptions = ContainerLogOptions(),
15+
val containerCustomizations: Map<BigDataService, ContainerCustomizationOptions> = emptyMap(),
16+
val healthChecks: Map<BigDataService, BigDataHealthCheckOptions> = emptyMap(),
1317
)
1418

1519
data class KerberosOptions(
@@ -61,6 +65,8 @@ data class HdfsOptions(
6165
val enabled: Boolean = false,
6266
val image: String = "apache/hadoop:3.5.0",
6367
val nameNodePort: Int = 8020,
68+
val dataNodePort: Int = 9866,
69+
val dataNodeHostname: String = "hdfs",
6470
val webPort: Int = 9870,
6571
val webTls: HttpTlsOptions = HttpTlsOptions(),
6672
val kerberos: KerberosAuthOptions = KerberosAuthOptions(
@@ -124,6 +130,7 @@ data class PortBindingOptions(
124130
val sameHostPorts: Boolean = false,
125131
val kerberosKdc: Int = 0,
126132
val hdfsNameNode: Int = 0,
133+
val hdfsDataNode: Int = 0,
127134
val hdfsWeb: Int = 0,
128135
val hdfsWebTls: Int = 0,
129136
val hiveMetastore: Int = 0,
@@ -157,3 +164,88 @@ data class ContainerLogOptions(
157164
val mode: ContainerLogMode = ContainerLogMode.NONE,
158165
val directory: String = "build/bigdata-test-container-logs",
159166
)
167+
168+
enum class BigDataHealthCheckMode {
169+
NONE,
170+
BASIC,
171+
CLI,
172+
}
173+
174+
data class BigDataHealthCheckOptions(
175+
val mode: BigDataHealthCheckMode = BigDataHealthCheckMode.BASIC,
176+
val timeoutSeconds: Long = 60,
177+
)
178+
179+
data class ContainerCustomizationOptions(
180+
val environment: Map<String, String> = emptyMap(),
181+
val files: List<ContainerFileTransferOptions> = emptyList(),
182+
val mounts: List<ContainerMountOptions> = emptyList(),
183+
val ports: List<ContainerPortOptions> = emptyList(),
184+
val customizers: List<BigDataContainerCustomizer> = emptyList(),
185+
) {
186+
fun merge(override: ContainerCustomizationOptions): ContainerCustomizationOptions =
187+
ContainerCustomizationOptions(
188+
environment = environment + override.environment,
189+
files = files + override.files,
190+
mounts = mounts + override.mounts,
191+
ports = ports + override.ports,
192+
customizers = customizers + override.customizers,
193+
)
194+
}
195+
196+
data class ContainerFileTransferOptions(
197+
val containerPath: String,
198+
val hostPath: String? = null,
199+
val content: ByteArray? = null,
200+
val fileMode: Int? = null,
201+
) {
202+
init {
203+
require(containerPath.isNotBlank()) { "Container file path must not be blank" }
204+
require((hostPath == null) != (content == null)) {
205+
"Exactly one of hostPath or content must be set for container file transfer"
206+
}
207+
}
208+
209+
companion object {
210+
@JvmStatic
211+
fun hostPath(hostPath: String, containerPath: String, fileMode: Int? = null): ContainerFileTransferOptions =
212+
ContainerFileTransferOptions(containerPath = containerPath, hostPath = hostPath, fileMode = fileMode)
213+
214+
@JvmStatic
215+
fun content(content: String, containerPath: String, fileMode: Int? = null): ContainerFileTransferOptions =
216+
ContainerFileTransferOptions(
217+
containerPath = containerPath,
218+
content = content.toByteArray(Charsets.UTF_8),
219+
fileMode = fileMode,
220+
)
221+
222+
@JvmStatic
223+
fun content(content: ByteArray, containerPath: String, fileMode: Int? = null): ContainerFileTransferOptions =
224+
ContainerFileTransferOptions(containerPath = containerPath, content = content, fileMode = fileMode)
225+
}
226+
}
227+
228+
data class ContainerMountOptions(
229+
val hostPath: String,
230+
val containerPath: String,
231+
val readOnly: Boolean = true,
232+
) {
233+
init {
234+
require(hostPath.isNotBlank()) { "Host mount path must not be blank" }
235+
require(containerPath.isNotBlank()) { "Container mount path must not be blank" }
236+
}
237+
}
238+
239+
data class ContainerPortOptions(
240+
val containerPort: Int,
241+
val hostPort: Int = 0,
242+
) {
243+
init {
244+
require(containerPort > 0) { "Container port must be positive" }
245+
require(hostPort >= 0) { "Host port must be 0 for random binding or a positive fixed port" }
246+
}
247+
}
248+
249+
fun interface BigDataContainerCustomizer {
250+
fun customize(container: GenericContainer<*>)
251+
}

0 commit comments

Comments
 (0)