Skip to content

Commit fbbab2e

Browse files
committed
feat: improve file handling in containers and enhance Kerberos material management
- Replaced `withFileSystemBind` with `withCopyFileToContainer` for better portability and security. - Added `copyKerberosMaterialFromContainer` for automatic post-start Kerberos material handling. - Introduced helper methods for managing Kerberos container file paths and synchronized file copying. - Updated `BigDataServiceContainer` to support `afterStart` actions for custom initialization. - Improved Spark example with centralized Jackson dependency versioning.
1 parent 3674ce2 commit fbbab2e

4 files changed

Lines changed: 96 additions & 14 deletions

File tree

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ class BigDataTestKit private constructor(
1313

1414
override fun start() {
1515
if (started) return
16-
serviceContainers.forEach { it.container.start() }
17-
serviceContainers.forEach { endpoints[it.service] = it.endpoint() }
16+
serviceContainers.forEach {
17+
it.container.start()
18+
it.afterStart()
19+
endpoints[it.service] = it.endpoint()
20+
}
1821
started = true
1922
}
2023

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

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import org.openprojectx.bigdata.test.core.KerberosAuthOptions
1010
import org.openprojectx.bigdata.test.core.KafkaOptions
1111
import org.openprojectx.bigdata.test.core.KerberosOptions
1212
import org.openprojectx.hive.docker.testcontainers.HiveMetastoreContainer
13-
import org.testcontainers.containers.BindMode
1413
import org.testcontainers.containers.GenericContainer
1514
import org.testcontainers.containers.Network
1615
import org.testcontainers.containers.PostgreSQLContainer
@@ -20,6 +19,7 @@ import org.testcontainers.containers.output.OutputFrame
2019
import org.testcontainers.images.builder.Transferable
2120
import org.testcontainers.lifecycle.Startable
2221
import org.testcontainers.utility.DockerImageName
22+
import org.testcontainers.utility.MountableFile
2323
import java.io.OutputStreamWriter
2424
import java.io.Closeable
2525
import java.nio.file.Files
@@ -77,13 +77,10 @@ internal class BigDataContainerFactory(
7777
addIfEnabled(options.kafka.kafkaUiKerberos)
7878
if (options.kafka.kerberos.enabled && options.kafka.kafkaUiEnabled) addPrincipal(options.kafka.kafkaUiKerberos)
7979
}
80-
writeContainerKerberosConf(kerberos)
81-
8280
val container = GenericBigDataContainer(kerberos.image)
8381
.withNetwork(network)
8482
.withNetworkAliases("kerby-kdc")
8583
.withServicePort(88, options.portBindings.hostPort(88, options.portBindings.kerberosKdc))
86-
.withFileSystemBind(kerberosDirectory(), "/var/lib/kerby")
8784
.withEnv("KERBY_REALM", kerberos.realm)
8885
.withEnv("KERBY_KDC_HOST", "kerby-kdc")
8986
.withEnv("KERBY_KDC_BIND_HOST", "0.0.0.0")
@@ -102,7 +99,11 @@ internal class BigDataContainerFactory(
10299
container.withEnv("KERBY_EXTRA_PRINCIPALS", users)
103100
}
104101

105-
return BigDataServiceContainer(BigDataService.KERBEROS, attachLogs("kerberos", container)) {
102+
return BigDataServiceContainer(
103+
service = BigDataService.KERBEROS,
104+
container = attachLogs("kerberos", container),
105+
afterStart = { copyKerberosMaterialFromContainer(container) },
106+
) {
106107
val localKrb5Conf = writeLocalKerberosConf(kerberos, container.host, container.getMappedPort(88))
107108
BigDataEndpoint(
108109
service = BigDataService.KERBEROS,
@@ -399,7 +400,10 @@ internal class BigDataContainerFactory(
399400
.waitingFor(Wait.forHttp("/subjects").forStatusCode(200).withStartupTimeout(Duration.ofMinutes(3)))
400401
if (kafka.tls.enabled && !kafka.kerberos.enabled) {
401402
container
402-
.withFileSystemBind(tlsMaterial.trustStorePath.toString(), "/etc/schema-registry/tls/truststore.p12", BindMode.READ_ONLY)
403+
.withCopyFileToContainer(
404+
MountableFile.forHostPath(tlsMaterial.trustStorePath),
405+
"/etc/schema-registry/tls/truststore.p12",
406+
)
403407
.withEnv("SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS", "SSL://kafka:9093")
404408
.withEnv("SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL", "SSL")
405409
.withEnv("SCHEMA_REGISTRY_KAFKASTORE_SSL_TRUSTSTORE_LOCATION", "/etc/schema-registry/tls/truststore.p12")
@@ -448,7 +452,7 @@ internal class BigDataContainerFactory(
448452
if (kafka.tls.enabled) {
449453
val trustStore = tlsMaterial.trustStorePath
450454
container
451-
.withFileSystemBind(trustStore.toString(), "/etc/kafka/tls/truststore.p12", BindMode.READ_ONLY)
455+
.withCopyFileToContainer(MountableFile.forHostPath(trustStore), "/etc/kafka/tls/truststore.p12")
452456
.withEnv("KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS", if (kafka.kerberos.enabled) "broker1.example.com:9092" else "kafka:9093")
453457
.withEnv("KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL", if (kafka.kerberos.enabled) "SASL_SSL" else "SSL")
454458
.withEnv("KAFKA_CLUSTERS_0_PROPERTIES_SSL_TRUSTSTORE_LOCATION", "/etc/kafka/tls/truststore.p12")
@@ -678,8 +682,8 @@ internal class BigDataContainerFactory(
678682
.withNetwork(network)
679683
.withNetworkAliases("$name-tls")
680684
.withServicePort(443, hostPort)
681-
.withFileSystemBind(config.toString(), "/usr/local/etc/haproxy/haproxy.cfg", BindMode.READ_ONLY)
682-
.withFileSystemBind(pem.toString(), "/usr/local/etc/haproxy/certs/service.pem", BindMode.READ_ONLY)
685+
.withCopyFileToContainer(MountableFile.forHostPath(config), "/usr/local/etc/haproxy/haproxy.cfg")
686+
.withCopyFileToContainer(MountableFile.forHostPath(pem), "/usr/local/etc/haproxy/certs/service.pem")
683687
.waitingFor(Wait.forListeningPort().withStartupTimeout(Duration.ofMinutes(2)))
684688
val proxied = attachLogs("$name-tls", container)
685689
proxied.start()
@@ -709,8 +713,11 @@ internal class BigDataContainerFactory(
709713
sanDomains = listOf("kafka", "broker1.example.com"),
710714
)
711715
container
712-
.withFileSystemBind(keyStore.path.toString(), "/etc/kafka/secrets/kafka.keystore.p12", BindMode.READ_ONLY)
713-
.withFileSystemBind(tlsMaterial.trustStorePath.toString(), "/etc/kafka/secrets/kafka.truststore.p12", BindMode.READ_ONLY)
716+
.withCopyFileToContainer(MountableFile.forHostPath(keyStore.path), "/etc/kafka/secrets/kafka.keystore.p12")
717+
.withCopyFileToContainer(
718+
MountableFile.forHostPath(tlsMaterial.trustStorePath),
719+
"/etc/kafka/secrets/kafka.truststore.p12",
720+
)
714721
.withCopyToContainer(Transferable.of(keyStore.password), "/etc/kafka/secrets/kafka.key.credentials")
715722
.withCopyToContainer(Transferable.of(keyStore.password), "/etc/kafka/secrets/kafka.keystore.credentials")
716723
.withCopyToContainer(Transferable.of(tlsMaterial.trustStorePassword), "/etc/kafka/secrets/kafka.truststore.credentials")
@@ -877,12 +884,79 @@ internal class BigDataContainerFactory(
877884

878885
private fun mountKerberos(container: GenericContainer<*>) {
879886
writeContainerKerberosConf(options.kerberos)
880-
container.withFileSystemBind(kerberosDirectory(), "/kerby", BindMode.READ_ONLY)
887+
copyKerberosFileToContainer(container, "/kerby/client/krb5.conf")
888+
kerberosContainerPaths().forEach { copyKerberosFileToContainer(container, it) }
881889
}
882890

883891
private fun kerberosDirectory(): String =
884892
kerberosDir?.toString() ?: error("Kerberos directory was not initialized")
885893

894+
private fun copyKerberosMaterialFromContainer(container: GenericContainer<*>) {
895+
val paths = kerberosContainerPaths()
896+
waitForKerberosFiles(container, paths)
897+
paths.forEach { containerPath ->
898+
copyKerberosFileFromContainer(
899+
container = container,
900+
source = containerPath.replace("/kerby/", "/var/lib/kerby/"),
901+
destination = containerPath,
902+
)
903+
}
904+
}
905+
906+
private fun copyKerberosFileFromContainer(
907+
container: GenericContainer<*>,
908+
source: String,
909+
destination: String,
910+
) {
911+
val path = hostKerberosMaterialPath(destination)
912+
Files.createDirectories(path.parent)
913+
container.copyFileFromContainer(source, path.toString())
914+
}
915+
916+
private fun waitForKerberosFiles(container: GenericContainer<*>, containerPaths: Set<String>) {
917+
val sources = containerPaths.map { it.replace("/kerby/", "/var/lib/kerby/") }
918+
val deadline = System.nanoTime() + Duration.ofSeconds(30).toNanos()
919+
while (true) {
920+
val missing = sources.filterNot { source ->
921+
container.execInContainer("sh", "-lc", "test -s '$source'").exitCode == 0
922+
}
923+
if (missing.isEmpty()) return
924+
if (System.nanoTime() >= deadline) {
925+
error("Kerberos KDC did not generate expected keytab files: ${missing.joinToString(", ")}")
926+
}
927+
Thread.sleep(250)
928+
}
929+
}
930+
931+
private fun copyKerberosFileToContainer(container: GenericContainer<*>, containerPath: String) {
932+
val path = hostKerberosMaterialPath(containerPath)
933+
Files.createDirectories(path.parent)
934+
if (Files.notExists(path)) {
935+
Files.write(path, ByteArray(0))
936+
}
937+
container.withCopyFileToContainer(MountableFile.forHostPath(path), containerPath)
938+
}
939+
940+
private fun kerberosContainerPaths(): Set<String> =
941+
buildSet {
942+
add("/kerby/keytabs/client.keytab")
943+
options.kerberos.users.forEach { add(it.keytabPath) }
944+
addIfEnabled(options.hdfs.kerberos)
945+
addIfEnabled(options.hiveMetastore.kerberos)
946+
addIfEnabled(options.kafka.kerberos)
947+
addIfEnabled(options.kafka.kafkaUiKerberos)
948+
if (options.kafka.kerberos.enabled && options.kafka.kafkaUiEnabled) add(options.kafka.kafkaUiKerberos.keytabPath)
949+
}
950+
951+
private fun MutableSet<String>.addIfEnabled(options: KerberosAuthOptions) {
952+
if (options.enabled) add(options.keytabPath)
953+
}
954+
955+
private fun hostKerberosMaterialPath(containerPath: String): Path {
956+
require(containerPath.startsWith("/kerby/")) { "Kerberos material path must be under /kerby: $containerPath" }
957+
return Path.of(kerberosDirectory()).resolve(containerPath.removePrefix("/kerby/"))
958+
}
959+
886960
private fun kerberosProperties(prefix: String, options: KerberosAuthOptions): Map<String, String> =
887961
if (options.enabled) {
888962
mapOf(

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ import org.testcontainers.containers.GenericContainer
77
internal data class BigDataServiceContainer(
88
val service: BigDataService,
99
val container: GenericContainer<*>,
10+
val afterStart: () -> Unit = {},
1011
val endpoint: () -> BigDataEndpoint,
1112
)

example/spark/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ dependencies {
4646
val apacheSparkVersion = libs.versions.spark.get()
4747
val apacheHadoopVersion = libs.versions.hadoop.get()
4848
val icebergVersion = libs.versions.iceberg.get()
49+
val apacheSparkJacksonVersion = "2.15.2"
4950
val testImplementationConfiguration = configurations.named("testImplementation")
5051
val testRuntimeOnlyConfiguration = configurations.named("testRuntimeOnly")
5152

@@ -67,6 +68,9 @@ fun createSparkRuntimeClasspath(name: String, dependencyLine: String) = configur
6768
"org.apache.spark" -> useVersion(apacheSparkVersion)
6869
"org.apache.hadoop" -> useVersion(apacheHadoopVersion)
6970
}
71+
if (requested.group.startsWith("com.fasterxml.jackson")) {
72+
useVersion(apacheSparkJacksonVersion)
73+
}
7074
}
7175
}
7276
}

0 commit comments

Comments
 (0)