Skip to content

Commit 63c5226

Browse files
committed
feat(hdfs): validate HDFS container environment variables and update configuration options
- Added `validateContainerEnvironment` to prevent conflicts with reserved Apache Hadoop environment variable formats. - Updated `spring-bigdata-test.toml` to adjust container log options and enable HDFS. - Introduced validation logic to handle unsupported configuration formats in the Hadoop image. - Enhanced `BigDataContainerFactory` with environment variable conflict detection. - Updated Gradle plugin to support project-relative container log directories.
1 parent 354d2da commit 63c5226

6 files changed

Lines changed: 89 additions & 6 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ HADOOP_OPTS = "-Dsun.security.krb5.debug=true"
9191

9292
Programmatic tests can also call `customizeContainer(...)` for last-resort Testcontainers access.
9393

94+
For HDFS, avoid arbitrary env names whose second token is an Apache Hadoop image config format such as `ENV`, `CONF`, `XML`, or `SH`. For example, `TEST_ENV=TEST` is parsed by `apache/hadoop:3.5.0` as an env-to-config instruction and fails before HDFS starts. Use a non-reserved name such as `TEST_VALUE`, or use the image convention intentionally, for example `CORE_XML_fs_defaultFS=...`.
95+
9496
Optional CLI health checks can run after container startup:
9597

9698
```toml

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -982,7 +982,10 @@ internal class BigDataContainerFactory(
982982
val customization = options.containerCustomizations[service] ?: return container
983983
customization.networkMode?.let { container.withNetworkMode(it) }
984984
customization.ports.forEach { container.addPort(it) }
985-
customization.environment.forEach { (name, value) -> container.withEnv(name, value) }
985+
customization.environment.forEach { (name, value) ->
986+
validateContainerEnvironment(service, name)
987+
container.withEnv(name, value)
988+
}
986989
customization.files.forEach { container.addFile(it) }
987990
customization.mounts.forEach { mount ->
988991
container.withFileSystemBind(
@@ -995,6 +998,27 @@ internal class BigDataContainerFactory(
995998
return container
996999
}
9971000

1001+
private fun validateContainerEnvironment(service: BigDataService, name: String) {
1002+
if (service != BigDataService.HDFS) return
1003+
val conflict = hadoopImageEnvToConfConflict(name) ?: return
1004+
error(
1005+
"HDFS container environment variable '$name' conflicts with the apache/hadoop image env-to-conf " +
1006+
"parser. '$conflict' is treated as a config file format by the image entrypoint. " +
1007+
"Use a non-reserved name, for example '${name}_VALUE'. For image-native config generation, " +
1008+
"use a supported format with a property key suffix, for example 'CORE_XML_fs_defaultFS'. " +
1009+
"Avoid the image's env/sh/cfg/conf formats; they are broken in apache/hadoop:3.5.0.",
1010+
)
1011+
}
1012+
1013+
private fun hadoopImageEnvToConfConflict(name: String): String? {
1014+
val parts = name.split(Regex("[_.]"))
1015+
if (parts.size < 2) return null
1016+
val format = parts[1].lowercase(Locale.ROOT)
1017+
if (format in HADOOP_IMAGE_ENV_TO_CONF_BROKEN_FORMATS) return format
1018+
if (parts.size == 2 && format in HADOOP_IMAGE_ENV_TO_CONF_SUPPORTED_FORMATS) return format
1019+
return null
1020+
}
1021+
9981022
private fun runCliHealthCheck(service: BigDataService, container: GenericContainer<*>, timeoutSeconds: Long) {
9991023
val command = cliHealthCheckCommand(service) ?: return
10001024
val result = container.execInContainer("sh", "-lc", withShellTimeout(timeoutSeconds, command))
@@ -1519,4 +1543,9 @@ internal class BigDataContainerFactory(
15191543
.${options.domain} = ${options.realm}
15201544
${options.domain} = ${options.realm}
15211545
""".trimIndent()
1546+
1547+
private companion object {
1548+
val HADOOP_IMAGE_ENV_TO_CONF_SUPPORTED_FORMATS = setOf("xml", "properties", "yaml", "yml")
1549+
val HADOOP_IMAGE_ENV_TO_CONF_BROKEN_FORMATS = setOf("env", "sh", "cfg", "conf")
1550+
}
15221551
}

doc/user-guide.adoc

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,48 @@ Mounts are useful for local debugging, but copied files are more reliable in CI
350350

351351
Available service names in container TOML are `kerberos`, `hdfs`, `hiveMetastore`, `hms`, `kafka`, `schemaRegistry`, `kafkaUi`, `localStackS3`, and `fakeGcs`.
352352

353+
==== HDFS Environment Variable Names
354+
355+
The HDFS service currently defaults to the `apache/hadoop:3.5.0` image. Its entrypoint runs the image's own `envtoconf.py` bootstrap before the `bigdata-test` HDFS startup command. This is an Apache Hadoop Docker image convention, not a general Hadoop runtime specification.
356+
357+
That image parser treats environment variable names whose second token is a known config format as instructions to generate files under `HADOOP_CONF_DIR`. Tokens are split on `_` and `.`. For example:
358+
359+
[source,text]
360+
----
361+
CORE_XML_fs_defaultFS=hdfs://hdfs:8020
362+
HDFS_XML_dfs_replication=1
363+
----
364+
365+
Those variables ask the image to generate `core.xml` and `hdfs.xml` content. This can coexist with `bigdata-test` when you intentionally use the image-native convention.
366+
367+
Do not use arbitrary HDFS custom environment variable names whose second token is one of the image's reserved config format names:
368+
369+
[source,text]
370+
----
371+
xml, properties, yaml, yml, env, sh, cfg, conf
372+
----
373+
374+
For example, this looks harmless but conflicts with the image parser:
375+
376+
[source,toml]
377+
----
378+
[containers.hdfs.env]
379+
TEST_ENV = "TEST"
380+
----
381+
382+
`TEST_ENV` is interpreted as "create `test.env`" with no config key. In `apache/hadoop:3.5.0`, the `env`, `sh`, `cfg`, and `conf` transforms are also buggy because they iterate parsed keys incorrectly. Names such as `TEST_ENV`, `TEST_ENV_FOO`, `APP_CONF`, and `FOO_SH_BAR` can fail before HDFS starts.
383+
384+
Use a non-reserved name instead:
385+
386+
[source,toml]
387+
----
388+
[containers.hdfs.env]
389+
TEST_VALUE = "TEST"
390+
MYAPP_ENV_VALUE = "TEST"
391+
----
392+
393+
For Hadoop XML settings, prefer the typed `bigdata-test` options or copied XML files. If you intentionally use the image convention, use one of the working formats with a property suffix, such as `CORE_XML_fs_defaultFS`. Avoid the image's `env`, `sh`, `cfg`, and `conf` formats with `apache/hadoop:3.5.0`.
394+
353395
Programmatic tests can use the same portable operations:
354396

355397
[source,kotlin]

example/spring-gradle-plugin/src/main/resources/spring-bigdata-extensions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[kerberosMaterial]
2-
enabled = true
2+
enabled = false
33

44
[s3Jceks]
55
enabled = true

example/spring-gradle-plugin/src/main/resources/spring-bigdata-test.toml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
[services]
2-
kerberos = true
3-
hdfsKerberos = true
2+
kerberos = false
3+
hdfsKerberos = false
4+
hdfs = true
5+
46
localStackS3 = true
7+
#schemaRegistry = true
8+
#kafka = true
59

610
[kerberos]
711
realm = "EXAMPLE.COM"
@@ -22,11 +26,16 @@ hdfsWeb = 9870
2226
localStackS3 = 4566
2327

2428
[containerLogs]
25-
mode = "STDOUT"
29+
mode = "FILE"
30+
directory = "build/container-logs"
31+
append = false
2632

2733
[containerLogLevels]
2834
kerberos = "DEBUG"
2935
hdfs = "DEBUG"
3036

3137
[containers.localstackS3.env]
3238
DISABLE_CORS_CHECKS = "1"
39+
40+
#[containers.hdfs.env]
41+
#TEST_ENV = "TEST"

gradle-plugin/src/main/kotlin/org/openprojectx/bigdata/test/gradle/BigDataTestGradleService.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,8 @@ abstract class BigDataTestGradleService : BuildService<BigDataTestGradleService.
182182
.withContainerLogs(
183183
ContainerLogOptions(
184184
mode = parameters.containerLogMode.get(),
185-
directory = parameters.containerLogDirectory.get(),
185+
directory = projectPath(parameters.containerLogDirectory.get())
186+
?: parameters.containerLogDirectory.get(),
186187
append = parameters.containerLogAppend.get(),
187188
),
188189
)

0 commit comments

Comments
 (0)