From ac0a92e1cdd9cf4013319e44b3310e3a72b16b46 Mon Sep 17 00:00:00 2001 From: Thomas Muller Date: Wed, 17 Apr 2024 17:51:58 +0200 Subject: [PATCH] Comprehensive updates across the codebase This commit reflects a multitude of adjustments across several resources: 1. Added `ContainerBuilderCallback` class that provides an interface for defining a callback to configure a container builder. 2. Extended `Container` interface with string argument variants for `putFile` and `getFile` methods - enhancing versatility & ease of use. 3. Revised `handleJobCompletion` of `K8sJobRuntime` class to bolster error handling by considering job failure scenarios. 4. Enhanced logging message accuracy in test class 'ShowCaseTests'. 5. Added `normalizeVolumeName` method in `K8sUtils`, improving volume name handling. 6. Test class `ContainerVolumeTests` was added, which includes a test case for mounting container file. 7. Updated `handleContainerStatuses` in test class `ContainerTaskTests` to cater for changes in Container state. 8. Edited `GenericContainer` to use the `ExperimentalStdlibApi` annotation, potentially enhancing future development options. 9. Improved `K8sUtils` by introducing a regex constant `INVALID_VOLUME_CHARS_REGEX` and a method `normalizeConfigMapName`. 10. Small formatting tweaks across classes for better code readability and cleanliness, including alterations in README.md --- NOTICE | 11 +- README.md | 1 - pom.xml | 5 - .../easycontainers/BaseContainerBuilder.kt | 16 +- .../ContainerBuilderCallback.kt | 20 ++ .../easycontainers/GenericContainer.kt | 1 + .../easycontainers/docker/DockerRuntime.kt | 65 +++++- .../kubernetes/K8sJobRuntime.kt | 16 +- .../easycontainers/kubernetes/K8sRuntime.kt | 219 ++++++++++-------- .../easycontainers/kubernetes/K8sUtils.kt | 18 +- .../acntech/easycontainers/model/Container.kt | 8 + .../easycontainers/model/ContainerBuilder.kt | 26 ++- .../easycontainers/model/ContainerFile.kt | 16 +- .../easycontainers/model/ContainerFileName.kt | 2 +- .../acntech/easycontainers/model/Verbosity.kt | 1 + .../no/acntech/easycontainers/model/Volume.kt | 5 +- .../easycontainers/util/io/FileUtils.kt | 3 - .../util/platform/PlatformUtils.kt | 68 +++--- .../ContainerFileTransferTests.kt | 5 +- .../easycontainers/ContainerTaskTests.kt | 5 +- .../easycontainers/ContainerVolumeTests.kt | 69 ++++++ .../acntech/easycontainers/ShowCaseTests.kt | 2 +- .../acntech/easycontainers/TestSupport.kt | 9 +- .../resources/env/{entry.sh => entrypoint.sh} | 0 src/test/resources/env/log-time.sh | 6 +- src/test/resources/env/test-dockerfile | 12 +- 26 files changed, 418 insertions(+), 191 deletions(-) create mode 100644 src/main/kotlin/no/acntech/easycontainers/ContainerBuilderCallback.kt create mode 100644 src/test/kotlin/test/acntech/easycontainers/ContainerVolumeTests.kt rename src/test/resources/env/{entry.sh => entrypoint.sh} (100%) diff --git a/NOTICE b/NOTICE index ae7d28af..e1ec020e 100644 --- a/NOTICE +++ b/NOTICE @@ -19,6 +19,7 @@ Additional notices: 3. This product includes software developed by the Docker Java project (https://github.com/docker-java/docker-java): - Docker Java API Client + - Docker Java Transport OkHttp The Docker Java API Client is used under the terms of the Apache License 2.0. 4. This product includes software from the Apache Commons project (http://commons.apache.org/): @@ -33,10 +34,6 @@ Additional notices: - Logback Classic Module The logging frameworks are used under the terms of the MIT License and the Eclipse Public License 1.0 respectively. -6. This product includes software developed by Michael Wiede (https://github.com/mwiede): - - JSch - The JSch library is used under the terms of the MIT License. - 7. This product includes software developed by Awaitility (https://github.com/awaitility/awaitility): - Awaitility The Awaitility library is used under the terms of the Apache License 2.0. @@ -45,7 +42,11 @@ Additional notices: - khttp The khttp library is used under the terms of the MIT License. -9. This product includes software from the JUnit Team (https://junit.org/): +9. This product includes software developed by Michael Wiede (https://github.com/mwiede): + - JSch + The JSch library is used under the terms of the MIT License. + +10. This product includes software from the JUnit Team (https://junit.org/): - JUnit Jupiter API - JUnit Jupiter Engine - JUnit Jupiter Params diff --git a/README.md b/README.md index b54efb5f..24a683cf 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,6 @@ Born from the practical insights gained from the [Testcontainers](https://www.te With Easycontainers, developers gain a versatile tool that streamlines container management, freeing them to focus more on development and less on the operational intricacies of containers. - ## Getting Started ### Starting a container diff --git a/pom.xml b/pom.xml index 208bbcd0..051fc216 100644 --- a/pom.xml +++ b/pom.xml @@ -129,11 +129,6 @@ netty-handler - - org.apache.commons - commons-compress - - org.apache.httpcomponents httpclient diff --git a/src/main/kotlin/no/acntech/easycontainers/BaseContainerBuilder.kt b/src/main/kotlin/no/acntech/easycontainers/BaseContainerBuilder.kt index eee87cb6..326ee8c0 100644 --- a/src/main/kotlin/no/acntech/easycontainers/BaseContainerBuilder.kt +++ b/src/main/kotlin/no/acntech/easycontainers/BaseContainerBuilder.kt @@ -3,6 +3,7 @@ package no.acntech.easycontainers import no.acntech.easycontainers.model.* import no.acntech.easycontainers.output.OutputLineCallback import no.acntech.easycontainers.util.collections.prettyPrint +import no.acntech.easycontainers.util.text.EQUALS import no.acntech.easycontainers.util.text.NEW_LINE import org.apache.commons.lang3.builder.ToStringBuilder import org.apache.commons.lang3.builder.ToStringStyle @@ -188,19 +189,8 @@ abstract class BaseContainerBuilder> : Contain return self() } - override fun withContainerFile( - name: ContainerFileName, - path: UnixDir, - data: Map, - keyValSeparator: String, - ): SELF { - val content = data.entries.joinToString(NEW_LINE) { (key, value) -> "$key$keyValSeparator$value" } - containerFiles[name] = ContainerFile(name, path, content) - return self() - } - - override fun withContainerFile(name: ContainerFileName, path: UnixDir, content: String): SELF { - containerFiles[name] = ContainerFile(name, path, content) + override fun withContainerFile(file: ContainerFile): SELF { + containerFiles[file.name] = file return self() } diff --git a/src/main/kotlin/no/acntech/easycontainers/ContainerBuilderCallback.kt b/src/main/kotlin/no/acntech/easycontainers/ContainerBuilderCallback.kt new file mode 100644 index 00000000..955c3d61 --- /dev/null +++ b/src/main/kotlin/no/acntech/easycontainers/ContainerBuilderCallback.kt @@ -0,0 +1,20 @@ +package no.acntech.easycontainers + +import no.acntech.easycontainers.model.ContainerBuilder + +/** + * An interface for defining a callback to configure a container builder. + * Implement the [ContainerBuilderCallback] interface to provide custom configuration logic for a container builder. + * + * @param T the type of container builder + */ +interface ContainerBuilderCallback { + + /** + * Configures a container builder. + * + * @param builder the container builder to configure + */ + fun configure(builder: ContainerBuilder<*>) + +} \ No newline at end of file diff --git a/src/main/kotlin/no/acntech/easycontainers/GenericContainer.kt b/src/main/kotlin/no/acntech/easycontainers/GenericContainer.kt index 1356a0e4..44ecaa68 100644 --- a/src/main/kotlin/no/acntech/easycontainers/GenericContainer.kt +++ b/src/main/kotlin/no/acntech/easycontainers/GenericContainer.kt @@ -43,6 +43,7 @@ open class GenericContainer( companion object { + @OptIn(ExperimentalStdlibApi::class) private val LEGAL_STATE_TRANSITIONS: Map> = mapOf( ContainerState.UNINITIATED to setOf(ContainerState.INITIALIZING), ContainerState.INITIALIZING to setOf(ContainerState.RUNNING, ContainerState.FAILED), diff --git a/src/main/kotlin/no/acntech/easycontainers/docker/DockerRuntime.kt b/src/main/kotlin/no/acntech/easycontainers/docker/DockerRuntime.kt index 00555ed9..d230fa35 100644 --- a/src/main/kotlin/no/acntech/easycontainers/docker/DockerRuntime.kt +++ b/src/main/kotlin/no/acntech/easycontainers/docker/DockerRuntime.kt @@ -16,11 +16,10 @@ import no.acntech.easycontainers.ContainerException import no.acntech.easycontainers.GenericContainer import no.acntech.easycontainers.model.* import no.acntech.easycontainers.util.io.FileUtils +import no.acntech.easycontainers.util.lang.asStringMap import no.acntech.easycontainers.util.lang.guardedExecution import no.acntech.easycontainers.util.platform.PlatformUtils -import no.acntech.easycontainers.util.text.EMPTY_STRING -import no.acntech.easycontainers.util.text.SPACE -import no.acntech.easycontainers.util.text.splitOnWhites +import no.acntech.easycontainers.util.text.* import org.awaitility.Awaitility.await import org.awaitility.core.ConditionTimeoutException import java.io.* @@ -138,6 +137,10 @@ internal class DockerRuntime( private var startedAt: Instant? = null + init { + log.debug("DockerRuntime using container builder:$NEW_LINE${container.builder}") + } + override fun getType(): ContainerPlatformType { return ContainerPlatformType.DOCKER } @@ -464,10 +467,11 @@ internal class DockerRuntime( { val image = container.getImage().toFQDN() val hostConfig = prepareHostConfig() + configureNetwork(hostConfig) val containerCmd = createContainerCommand(image, hostConfig).also { - log.debug("containerCmd created: $it") + log.debug("containerCmd created:$NEW_LINE${it.asStringMap()}") } containerCmd.exec().id.also { @@ -624,34 +628,71 @@ internal class DockerRuntime( } private fun createDockerVolumes(hostConfig: HostConfig): List { - val volumeMap = container.getVolumes().associateWith { Volume(it.mountPath.value) } + val volumes = container.getVolumes() + val volumeMap = volumes.associateWith { + Volume(it.mountPath.value) + } val dockerVolumeNames = getExistingVolumeNames() - val binds = container.getVolumes().filter { it.memoryBacked } + val volumeBinds = volumes + .filter { !it.memoryBacked } .map { volume -> createBind(volume, volumeMap[volume], dockerVolumeNames) } - val tmpfsMounts = container.getVolumes().filter { it.memoryBacked } + val fileBinds = container.builder.containerFiles.map { (name, file) -> + createContainerFileBind(file) + } + + val tmpfsMounts = volumes + .filter { it.memoryBacked } .map { volume -> createTmpfsMount(volume) } - configureHostConfigVolumes(hostConfig, binds, tmpfsMounts) + configureHostConfigVolumes(hostConfig, volumeBinds + fileBinds, tmpfsMounts) return volumeMap.values.toList() } + private fun createContainerFileBind(containerFile: ContainerFile): Bind { + val hostFile = containerFile.hostFile ?: File.createTempFile(containerFile.name.value, null).toPath() + + // If content is not null, write content to this file + containerFile.content?.let { content -> + hostFile.toFile().writeText(content) + } + + val actualBindPath = PlatformUtils.convertToDockerPath(hostFile) + + // Get the complete path including the filename as the mount point + val mountPath = "${containerFile.mountPath}$FORWARD_SLASH${containerFile.name}" + + // Finally, create a Docker bind + val bind = Bind(actualBindPath, Volume(mountPath)) + + return bind.also { + log.info( + "Using host file '${actualBindPath}' for container file '${containerFile.name}'" + + " with mount-path '$mountPath'" + ) + } + } + private fun createBind( volume: no.acntech.easycontainers.model.Volume, dockerVolume: Volume?, dockerVolumeNames: Set, ): Bind { val volumeName = volume.name.value + return if (dockerVolumeNames.contains(volumeName)) { log.info("Using existing named Docker volume '$volumeName' with mount-path '${volume.mountPath}'") Bind(volumeName, dockerVolume) + } else { log.info("Using hostDir '${volume.hostDir}' for volume '$volumeName'") - val actualHostDir = - getActualHostDir(volume.hostDir ?: throw ContainerException("Volume '$volumeName' must have a hostDir")) - Bind(actualHostDir, dockerVolume) + + volume.hostDir?.let { hostDir -> + val actualHostDir = getActualHostDir(hostDir) + Bind(actualHostDir, dockerVolume) + } ?: throw ContainerException("Volume '$volumeName' must have a host-dir") } } @@ -720,7 +761,7 @@ internal class DockerRuntime( } private fun configureVolumes(cmd: CreateContainerCmd, hostConfig: HostConfig) { - if (container.getVolumes().isNotEmpty()) { + if (container.getVolumes().isNotEmpty() || container.builder.containerFiles.isNotEmpty()) { val volumes = createDockerVolumes(hostConfig) cmd.withVolumes(*volumes.toTypedArray()) } else { diff --git a/src/main/kotlin/no/acntech/easycontainers/kubernetes/K8sJobRuntime.kt b/src/main/kotlin/no/acntech/easycontainers/kubernetes/K8sJobRuntime.kt index 5e1f4951..5342f744 100644 --- a/src/main/kotlin/no/acntech/easycontainers/kubernetes/K8sJobRuntime.kt +++ b/src/main/kotlin/no/acntech/easycontainers/kubernetes/K8sJobRuntime.kt @@ -186,6 +186,7 @@ class K8sJobRuntime( private fun handleJobCompletion(condition: JobCondition) { if ("True" == condition.status) { + val completionDateTimeVal = job.status.completionTime completionDateTimeVal?.let { finishedAt = Instant.parse(completionDateTimeVal) @@ -198,7 +199,20 @@ class K8sJobRuntime( log.info("Job '$jobName' took approximately: $duration") } } - container.changeState(ContainerState.STOPPED) + + job.status.failed?.let { failed -> + if (failed > 0) { + log.error("Job '$jobName' failed with $failed failed pods") + container.changeState(ContainerState.FAILED) + } else { + log.info("Job '$jobName' completed successfully") + container.changeState(ContainerState.STOPPED) + } + } ?: run { + log.info("Job '$jobName' completed successfully") + container.changeState(ContainerState.STOPPED) + } + completionLatch.countDown() log.trace("Latch decremented, job '$jobName' completed") } diff --git a/src/main/kotlin/no/acntech/easycontainers/kubernetes/K8sRuntime.kt b/src/main/kotlin/no/acntech/easycontainers/kubernetes/K8sRuntime.kt index 59f3d5c3..46c66b4b 100644 --- a/src/main/kotlin/no/acntech/easycontainers/kubernetes/K8sRuntime.kt +++ b/src/main/kotlin/no/acntech/easycontainers/kubernetes/K8sRuntime.kt @@ -13,18 +13,16 @@ import no.acntech.easycontainers.ContainerException import no.acntech.easycontainers.GenericContainer import no.acntech.easycontainers.kubernetes.K8sConstants.APP_LABEL import no.acntech.easycontainers.kubernetes.K8sConstants.MEDIUM_MEMORY_BACKED +import no.acntech.easycontainers.kubernetes.K8sUtils.normalizeConfigMapName import no.acntech.easycontainers.kubernetes.K8sUtils.normalizeLabelValue +import no.acntech.easycontainers.kubernetes.K8sUtils.normalizeVolumeName import no.acntech.easycontainers.model.* import no.acntech.easycontainers.model.ContainerState import no.acntech.easycontainers.util.lang.guardedExecution import no.acntech.easycontainers.util.lang.prettyPrintMe -import no.acntech.easycontainers.util.text.BACK_SLASH -import no.acntech.easycontainers.util.text.FORWARD_SLASH -import no.acntech.easycontainers.util.text.NEW_LINE -import no.acntech.easycontainers.util.text.SPACE +import no.acntech.easycontainers.util.text.* import org.awaitility.Awaitility import org.awaitility.core.ConditionTimeoutException -import java.io.File import java.io.FileNotFoundException import java.io.InputStream import java.io.OutputStream @@ -43,7 +41,6 @@ import kotlin.collections.set import kotlin.io.path.exists import kotlin.io.path.isDirectory - /** * Represents an abstract Kubernetes runtime for a container, capturing common functionality and properties for both * Kubernetes Jobs and Deployments. @@ -98,6 +95,8 @@ abstract class K8sRuntime( const val SERVICE_NAME_SUFFIX = "-service" const val PV_NAME_SUFFIX = "-pv" const val PVC_NAME_SUFFIX = "-pvc" + const val CONFIG_MAP_NAME_SUFFIX = "-config-map" + const val VOLUME_NAME_SUFFIX = "-volume" private fun mapPodPhaseToContainerState(podPhase: PodPhase?): ContainerState { return when (podPhase) { @@ -601,40 +600,39 @@ abstract class K8sRuntime( volumeMounts: MutableList, ) { container.builder.containerFiles.forEach { (name, configFile) -> - handleContainerFile(name, configFile, volumes, volumeMounts) + handleContainerFile(configFile, volumes, volumeMounts) } } private fun handleContainerFile( - name: ContainerFileName, - configFile: ContainerFile, + file: ContainerFile, volumes: MutableList, volumeMounts: MutableList, ) { - log.trace("Creating name -> config map mapping: $name -> $configFile") - val (mountPath, fileName) = extractMountPathAndFileName(configFile) + log.trace("Creating name -> config map mapping: ${file.name} -> $file") + + val baseName = "container-file-${file.name.value}" + val configMapName = normalizeConfigMapName( + "$baseName-${UUID.randomUUID().toString().truncate(5)}$CONFIG_MAP_NAME_SUFFIX}" + ) + val volumeName = normalizeVolumeName("$baseName$VOLUME_NAME_SUFFIX") + val fileName = file.name.value - val configMap = createConfigMap(name, fileName, configFile) - handleConfigMapCreation(configMap) + val configMap = createConfigMap(configMapName, fileName, file) + applyConfigMap(configMap) - val volume = createVolume(name) + val volume = createConfigMapVolume(volumeName, configMapName) volumes.add(volume) - val volumeMount = createVolumeMount(name, mountPath, fileName) + // Since we're using the same file name as the key, we don't need to specify the subPath + val volumeMount = createVolumeMount(volumeName, file.mountPath.value) volumeMounts.add(volumeMount) } - private fun extractMountPathAndFileName(configFile: ContainerFile): Pair { - val mountPath = File(configFile.mountPath.value).parent?.replace(BACK_SLASH, FORWARD_SLASH) ?: FORWARD_SLASH - val fileName = File(configFile.mountPath.value).name - - return Pair(mountPath, fileName) - } - - private fun createConfigMap(name: ContainerFileName, fileName: String, configFile: ContainerFile): ConfigMap { + private fun createConfigMap(name: String, fileName: String, configFile: ContainerFile): ConfigMap { val configMap = ConfigMapBuilder() .withNewMetadata() - .withName(name.value) + .withName(name) .withNamespace(namespace) .addToLabels(createDefaultLabels()) .endMetadata() @@ -644,7 +642,7 @@ abstract class K8sRuntime( return configMap } - private fun handleConfigMapCreation(configMap: ConfigMap) { + private fun applyConfigMap(configMap: ConfigMap) { val configMapResource: Resource = client.configMaps() .inNamespace(namespace) @@ -655,91 +653,92 @@ abstract class K8sRuntime( .withTimeout(30, TimeUnit.SECONDS) .delete() .also { - log.info("Deleted existing k8s config map: $it") + log.info("Deleted existing k8s config map:$NEW_LINE${it.prettyPrintMe()}") } } configMapResource.create().also { configMaps.add(it) - log.info("Created a k8s config map: $it") + log.info("Created k8s config map:$NEW_LINE${it.prettyPrintMe()}") log.debug("ConfigMap YAML: ${Serialization.asYaml(configMap)}") } } - private fun createVolume(name: ContainerFileName): Volume { + private fun createConfigMapVolume(volumeName: String, configMapName: String): Volume { val volume = VolumeBuilder() - .withName(name.value) + .withName(volumeName) .withNewConfigMap() - .withName(name.value) + .withName(configMapName) .endConfigMap() .build() return volume } - private fun createVolumeMount(name: ContainerFileName, mountPath: String, fileName: String): VolumeMount { - val volumeMount = VolumeMountBuilder() - .withName(name.value) + private fun createVolumeMount(name: String, mountPath: String, fileName: String? = null): VolumeMount { + return VolumeMountBuilder() + .withName(name) .withMountPath(mountPath) - .withSubPath(fileName) // Mount only the specific file within the directory - .build() - - return volumeMount + .apply { + fileName?.let(this::withSubPath) + }.build() } private fun handlePersistentVolumes( volumes: MutableList, volumeMounts: MutableList, ) { - container.builder.volumes.filter { - !it.memoryBacked - }.forEach { volume -> - - // Derive the PVC name from the volume name - val pvcName = "${volume.name}$PVC_NAME_SUFFIX" - - // Create the Volume using the existing PVC - val k8sVolume = VolumeBuilder() - .withName(volume.name.value) - .withNewPersistentVolumeClaim() - .withClaimName(pvcName) - .endPersistentVolumeClaim() - .build() - volumes.add(k8sVolume) - - // Create the VolumeMount - val volumeMount = VolumeMountBuilder() - .withName(volume.name.value) - .withMountPath(volume.mountPath.value) - .build() - volumeMounts.add(volumeMount) - - log.info("Created persistent volume: ${volume.name.value}") - } + container.builder.volumes + .filter { !it.memoryBacked } + .forEach { volume -> + + // Derive the PVC name from the volume name + val pvcName = "${volume.name}$PVC_NAME_SUFFIX" + + // Create the Volume using the existing PVC + val k8sVolume = VolumeBuilder() + .withName(volume.name.value) + .withNewPersistentVolumeClaim() + .withClaimName(pvcName) + .endPersistentVolumeClaim() + .build() + volumes.add(k8sVolume) + + // Create the VolumeMount + val volumeMount = VolumeMountBuilder() + .withName(volume.name.value) + .withMountPath(volume.mountPath.value) + .build() + volumeMounts.add(volumeMount) + + log.info("Created persistent volume: ${volume.name}") + } } private fun handleMemoryBackedVolumes(volumes: MutableList, volumeMounts: MutableList) { - container.builder.volumes.filter { it.memoryBacked }.forEach { volume -> - val emptyDir = EmptyDirVolumeSource() - emptyDir.medium = MEDIUM_MEMORY_BACKED - volume.memory?.let { - emptyDir.sizeLimit = Quantity(it.toFormattedString()) - } + container.builder.volumes + .filter { it.memoryBacked } + .forEach { volume -> + val emptyDir = EmptyDirVolumeSource() + emptyDir.medium = MEDIUM_MEMORY_BACKED + volume.memory?.let { + emptyDir.sizeLimit = Quantity(it.toFormattedString()) + } - val k8sVolume = VolumeBuilder() - .withName(volume.name.value) - .withEmptyDir(emptyDir) - .build() - volumes.add(k8sVolume) + val k8sVolume = VolumeBuilder() + .withName(volume.name.value) + .withEmptyDir(emptyDir) + .build() + volumes.add(k8sVolume) - val volumeMount = VolumeMountBuilder() - .withName(volume.name.value) - .withMountPath(volume.mountPath.value) - .build() - volumeMounts.add(volumeMount) + val volumeMount = VolumeMountBuilder() + .withName(volume.name.value) + .withMountPath(volume.mountPath.value) + .build() + volumeMounts.add(volumeMount) - log.info("Created memory-backed volume: ${volume.name.value}") - } + log.info("Created memory-backed volume: ${volume.name}") + } } private fun extractOurDeploymentName(): String? { @@ -788,6 +787,7 @@ abstract class K8sRuntime( } pod = refreshedPod + refreshContainer() updatePodState() } @@ -854,40 +854,63 @@ abstract class K8sRuntime( // Check the container status val containerStatus = containerStatuses.first() + // Update start time if the container is running containerStatus.state.running?.startedAt?.let { startedAt = Instant.parse(it) } // Handle container termination information - return handleTerminatedContainerState(containerStatus, newState) + return handlePossibleTerminatedContainerState(containerStatus, newState) } - private fun handleTerminatedContainerState(containerStatus: ContainerStatus, newState: ContainerState): ContainerState { - containerStatus.state.terminated?.let { terminatedState -> - log.info( - "Container ${containerStatus.name} terminated with signal '${terminatedState.signal}', " + - "reason: ${terminatedState.reason}, message: '${terminatedState.message}'" - ) + private fun handlePossibleTerminatedContainerState( + containerStatus: ContainerStatus, + newState: ContainerState, + ): ContainerState { + var resultState = newState - // Update finish time and exit code upon termination - terminatedState.finishedAt?.let { - finishedAt = Instant.parse(it) + listOfNotNull(containerStatus.state.terminated, containerStatus.lastState.terminated) + .firstOrNull() + ?.let { terminatedState -> + resultState = handleTerminatedContainer(containerStatus, terminatedState) } - log.debug("Exit code: ${terminatedState.exitCode}") + return resultState + } - terminatedState.exitCode?.let { code -> - exitCode = code.also { - log.info("Container '${getName()}' exited with code: $code") - } + private fun handleTerminatedContainer( + containerStatus: ContainerStatus, + terminatedState: ContainerStateTerminated, + ): ContainerState { + log.info( + "Container ${containerStatus.name} terminated with signal '${terminatedState.signal}', " + + "reason: ${terminatedState.reason}, message: '${terminatedState.message}'" + ) + + // Update finish time and exit code upon termination + terminatedState.finishedAt?.let { + finishedAt = Instant.parse(it) + } + + terminatedState.exitCode?.let { code -> + exitCode = code.also { + log.info("Container '${getName()}' exited with code: $code") } + } + // Check if the container was terminated due to an error + return if (terminatedState.reason.lowercase().contains("error")) { + log.warn( + "Container '${containerStatus.name}' terminated due to '" + + " ${terminatedState.reason}': ${terminatedState.message}" + ) + ContainerState.FAILED + + } else { // When a container is terminated, we consider the pod to be STOPPED - return ContainerState.STOPPED + ContainerState.STOPPED } - - return newState } diff --git a/src/main/kotlin/no/acntech/easycontainers/kubernetes/K8sUtils.kt b/src/main/kotlin/no/acntech/easycontainers/kubernetes/K8sUtils.kt index e57b6344..4b802388 100644 --- a/src/main/kotlin/no/acntech/easycontainers/kubernetes/K8sUtils.kt +++ b/src/main/kotlin/no/acntech/easycontainers/kubernetes/K8sUtils.kt @@ -5,6 +5,7 @@ import io.fabric8.kubernetes.client.KubernetesClientException import no.acntech.easycontainers.kubernetes.K8sConstants.ENV_KUBERNETES_SERVICE_HOST import no.acntech.easycontainers.kubernetes.K8sConstants.ENV_KUBERNETES_SERVICE_PORT import no.acntech.easycontainers.kubernetes.K8sConstants.SERVICE_ACCOUNT_PATH +import no.acntech.easycontainers.util.text.EMPTY_STRING import no.acntech.easycontainers.util.text.FORWARD_SLASH import no.acntech.easycontainers.util.text.HYPHEN import java.nio.file.Files @@ -30,11 +31,14 @@ object K8sUtils { private val MULTIPLE_HYPHENS_REGEX = "-+".toRegex() + private val INVALID_VOLUME_CHARS_REGEX = "[^a-z0-9-]".toRegex() + private val INSTANT_LABEL_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'").withZone(ZoneOffset.UTC) // Define maximum lengths private const val MAX_PREFIX_LENGTH = 253 private const val MAX_LABEL_LENGTH = 63 + private const val MAX_VOLUME_NAME_LENGTH = 63 /** * Checks whether the current application is running inside a cluster. @@ -95,6 +99,10 @@ object K8sUtils { return LABEL_REGEX.matches(value) } + fun normalizeConfigMapName(name: String): String { + return normalizeLabelKey(name) + } + /** * Normalizes the given label key by separating the prefix and name if a forward slash is present, * normalizing the prefix and name by replacing invalid label characters with hyphens, removing @@ -110,7 +118,7 @@ object K8sUtils { val parts = labelKey.split(FORWARD_SLASH) val (prefix, name) = when { parts.size > 1 -> Pair(parts[0], parts[1]) - else -> Pair("", labelKey) + else -> Pair(EMPTY_STRING, labelKey) } // Normalize prefix and name @@ -144,6 +152,14 @@ object K8sUtils { .trim { it == '-' || it == '_' } } + fun normalizeVolumeName(value: String): String { + return value.lowercase() + .replace(INVALID_VOLUME_CHARS_REGEX, "-") + .replace(MULTIPLE_HYPHENS_REGEX, "-") + .take(MAX_VOLUME_NAME_LENGTH) + .trim('-') + } + /** * Converts an Instant to a legal label value. * diff --git a/src/main/kotlin/no/acntech/easycontainers/model/Container.kt b/src/main/kotlin/no/acntech/easycontainers/model/Container.kt index c48435f3..e76f0290 100644 --- a/src/main/kotlin/no/acntech/easycontainers/model/Container.kt +++ b/src/main/kotlin/no/acntech/easycontainers/model/Container.kt @@ -214,6 +214,10 @@ interface Container { */ fun putFile(localFile: Path, remoteDir: UnixDir, remoteFilename: String? = null): Long + fun putFile(localFile: String, remoteDir: String, remoteFilename: String? = null): Long { + return putFile(Path.of(localFile), UnixDir.of(remoteDir), remoteFilename) + } + /** * Downloads a file from the container. * @@ -227,6 +231,10 @@ interface Container { */ fun getFile(remoteDir: UnixDir, remoteFilename: String, localPath: Path? = null): Path + fun getFile(remoteDir: String, remoteFilename: String, localPath: String? = null): Path { + return getFile(UnixDir.of(remoteDir), remoteFilename, localPath?.let { Path.of(it) }) + } + /** * Uploads a directory to the container. Note that the uploaded files and directoroes include the * the directory itself and all its contents. diff --git a/src/main/kotlin/no/acntech/easycontainers/model/ContainerBuilder.kt b/src/main/kotlin/no/acntech/easycontainers/model/ContainerBuilder.kt index ec012ce0..0aa87980 100644 --- a/src/main/kotlin/no/acntech/easycontainers/model/ContainerBuilder.kt +++ b/src/main/kotlin/no/acntech/easycontainers/model/ContainerBuilder.kt @@ -2,6 +2,7 @@ package no.acntech.easycontainers.model import no.acntech.easycontainers.output.OutputLineCallback import no.acntech.easycontainers.util.text.COLON +import no.acntech.easycontainers.util.text.NEW_LINE import no.acntech.easycontainers.util.text.SPACE import java.math.BigInteger import java.time.Duration @@ -357,6 +358,8 @@ interface ContainerBuilder> { */ fun withOutputLineCallback(outputLineCallback: OutputLineCallback): SELF + fun withContainerFile(file: ContainerFile): SELF + /** * Adds a container file to the builder. * @@ -371,18 +374,35 @@ interface ContainerBuilder> { path: UnixDir, data: Map, keyValSeparator: String = "$COLON$SPACE", - ): SELF + ): SELF { + val content = data.entries.joinToString("\n") { (key, value) -> "$key$keyValSeparator$value" } + return withContainerFile(name, path, content) + } + + fun withContainerFile( + name: String, + path: String, + data: Map, + keyValSeparator: String = "$COLON$SPACE", + ): SELF { + return withContainerFile(ContainerFileName.of(name), UnixDir.of(path), data, keyValSeparator) + } /** * Adds a file to the container with the specified name, path, and content. - * This method creates a new instance of the ContainerFile class and stores it in the containerFiles map. * * @param name The name of the file in the container * @param path The path in the container where the file will be mounted. * @param content The content of the file * @return The updated ContainerBuilder instance. */ - fun withContainerFile(name: ContainerFileName, path: UnixDir, content: String): SELF + fun withContainerFile(name: ContainerFileName, path: UnixDir, content: String): SELF { + return withContainerFile(ContainerFile(name, path, content)) + } + + fun withContainerFile(name: String, mountPath: String, content: String): SELF { + return withContainerFile(ContainerFileName.of(name), UnixDir.of(mountPath), content) + } /** * Sets the volume name and mount path for the current instance of the object. diff --git a/src/main/kotlin/no/acntech/easycontainers/model/ContainerFile.kt b/src/main/kotlin/no/acntech/easycontainers/model/ContainerFile.kt index 8865bd5a..503a4693 100644 --- a/src/main/kotlin/no/acntech/easycontainers/model/ContainerFile.kt +++ b/src/main/kotlin/no/acntech/easycontainers/model/ContainerFile.kt @@ -5,11 +5,8 @@ import java.nio.file.Path /** * Value object representing a file to be added or mapped into a container. *
    - *
  • For Docker this becomes a bind mount, and the content parameter will be ignored if the localFile parameter - * is set.
  • - * - *
  • For Docker, if the localFile parameter is not set, the content parameter must be set and EasyContainers will create a - * temporary file with the contents of the 'content' parameter and use that as the source for the bind mount.
  • + *
  • For Docker this becomes a bind mount, and if hostFile is set, the file will be mounted from the host, otherwise + * a temporary file will be as the source for the bind mount. Either way the file will be populated with 'content' if set
  • * *
  • Note that a in Docker a bind mount is a synchronization mechanism between the host and the container, and any changes to * the file made by any of the parties will be reflected in the other party.
  • @@ -19,13 +16,14 @@ import java.nio.file.Path * hence avoiding obscuring an existing directory in the container's file system. *
* - * @param containerFileName The name of the file in the container + * @param name The name of the file in the container. * @param mountPath The path in the container where the file will be mounted. - * @param content Optional content - valid for both Docker and Kubernetes - * @param hostFile Only valid for Docker - if set, the content parameter will be ignored + * @param content Optional content - valid for both Docker and Kubernetes - if set, the mounted file will be populated with this + * content. + * @param hostFile Only valid for Docker - if not set a temporary file will be created and used as the source for the bind mount. */ data class ContainerFile( - val containerFileName: ContainerFileName, + val name: ContainerFileName, val mountPath: UnixDir, val content: String? = null, val hostFile: Path? = null, diff --git a/src/main/kotlin/no/acntech/easycontainers/model/ContainerFileName.kt b/src/main/kotlin/no/acntech/easycontainers/model/ContainerFileName.kt index 210db750..92911a50 100644 --- a/src/main/kotlin/no/acntech/easycontainers/model/ContainerFileName.kt +++ b/src/main/kotlin/no/acntech/easycontainers/model/ContainerFileName.kt @@ -12,7 +12,7 @@ value class ContainerFileName(val value: String) : SimpleValueObject { companion object { - private val REGEXP: Regex = "^[a-zA-Z0-9_-]\$".toRegex() + private val REGEXP: Regex = "^[a-zA-Z0-9_.-]+$".toRegex() private val VALIDATOR = StringValueObjectValidator( minLength = 1, diff --git a/src/main/kotlin/no/acntech/easycontainers/model/Verbosity.kt b/src/main/kotlin/no/acntech/easycontainers/model/Verbosity.kt index 0b97631c..26888630 100644 --- a/src/main/kotlin/no/acntech/easycontainers/model/Verbosity.kt +++ b/src/main/kotlin/no/acntech/easycontainers/model/Verbosity.kt @@ -22,6 +22,7 @@ enum class Verbosity(val value: String) { TRACE("trace"); companion object { + @OptIn(ExperimentalStdlibApi::class) fun of(value: String): Verbosity? { val valueLower = value.lowercase() return entries.find { it.value == valueLower } diff --git a/src/main/kotlin/no/acntech/easycontainers/model/Volume.kt b/src/main/kotlin/no/acntech/easycontainers/model/Volume.kt index 4ef30fb3..3a6f29ca 100644 --- a/src/main/kotlin/no/acntech/easycontainers/model/Volume.kt +++ b/src/main/kotlin/no/acntech/easycontainers/model/Volume.kt @@ -27,4 +27,7 @@ data class Volume( val hostDir: Path? = null, // Only valid for Docker val memoryBacked: Boolean = false, val memory: Memory? = null, -) \ No newline at end of file +) { + constructor(name: String, mountPath: String) : this(VolumeName.of(name), UnixDir.of(mountPath), null, false, null) + +} \ No newline at end of file diff --git a/src/main/kotlin/no/acntech/easycontainers/util/io/FileUtils.kt b/src/main/kotlin/no/acntech/easycontainers/util/io/FileUtils.kt index ebd319c0..d733948a 100644 --- a/src/main/kotlin/no/acntech/easycontainers/util/io/FileUtils.kt +++ b/src/main/kotlin/no/acntech/easycontainers/util/io/FileUtils.kt @@ -141,9 +141,6 @@ object FileUtils { @Throws(IOException::class) fun untarFile(tarFile: File, destination: Path = Files.createTempDirectory("untar-").toAbsolutePath()): Path { require(tarFile.exists() && tarFile.isFile) { "The provided file '$tarFile' is not a valid file." } -// require(Files.isDirectory(destination) || (!Files.exists(destination) && destination.parent.toFile().canWrite())) { -// "The provided destination '$destination' must be an existing directory or a non-existing file that can be written to." -// } TarArchiveInputStream(BufferedInputStream(FileInputStream(tarFile))).use { tis -> val entry = tis.nextEntry diff --git a/src/main/kotlin/no/acntech/easycontainers/util/platform/PlatformUtils.kt b/src/main/kotlin/no/acntech/easycontainers/util/platform/PlatformUtils.kt index 83fab98b..2de1d670 100644 --- a/src/main/kotlin/no/acntech/easycontainers/util/platform/PlatformUtils.kt +++ b/src/main/kotlin/no/acntech/easycontainers/util/platform/PlatformUtils.kt @@ -1,9 +1,6 @@ package no.acntech.easycontainers.util.platform -import no.acntech.easycontainers.util.text.EMPTY_STRING -import no.acntech.easycontainers.util.text.FORWARD_SLASH -import no.acntech.easycontainers.util.text.NEW_LINE -import no.acntech.easycontainers.util.text.splitOnWhites +import no.acntech.easycontainers.util.text.* import org.apache.commons.exec.CommandLine import org.apache.commons.exec.DefaultExecutor import org.apache.commons.exec.PumpStreamHandler @@ -224,14 +221,15 @@ object PlatformUtils { // If starts with \\wsl$\, then it's already a WSL path, and we need convert it to a Linux-style path return if (windowsPath.startsWith("\\\\wsl\$")) { - val wslPath = windowsPath.replace("\\", "/") - val pathDistroName = wslPath.split("/")[3] + val wslPath = windowsPath.replace(BACK_SLASH, FORWARD_SLASH) + val pathDistroName = wslPath.split(FORWARD_SLASH)[3] wslPath.replaceFirst("\\\\wsl$\\\\$pathDistroName", EMPTY_STRING) + } else { // Convert Windows-style path to WSL share path (e.g. /mnt/c/Users/path) val driveLetter = windowsPath.substring(0, 1).lowercase() // Extract the drive letter and convert to lowercase - val windowsPathWithoutDrive = - windowsPath.substring(2).replace("\\", "/") // Remove the drive letter and replace backslashes with forward slashes + val windowsPathWithoutDrive = windowsPath.substring(2) + .replace(BACK_SLASH, FORWARD_SLASH) // Remove the drive letter and replace backslashes with forward slashes "/mnt/${driveLetter}/$windowsPathWithoutDrive" } } @@ -258,34 +256,48 @@ object PlatformUtils { * Example: "C:\Users\path" -> "/mnt/c/Users/path" (WSL) or "C:/Users/path" (Docker Desktop) */ fun convertToDockerPath(path: Path): String { - // For Linux or Mac, return the absolute path - if (isLinux() || isMac()) { - return path.toAbsolutePath().toString() - } + val convertedPath: String - // For Windows with Docker Desktop installed, return the Windows path in Docker format - if (isWindows() && isDockerDesktopOnWindows()) { - // Convert Path to a Windows-style path recognizable by Docker Desktop - // Dynamically handle the drive letter + if (isWindows()) { + // Convert Path to a Windows-style path val absolutePath = path.toAbsolutePath().toString() - val driveLetter = absolutePath.substring(0, 1).lowercase() // Extract the drive letter and convert to lowercase - return absolutePath.replace("\\", "/").replaceFirst("${driveLetter}:/", "/${driveLetter}/") - } + val driveLetter = absolutePath.substring(0, 1) + + if (isDockerDesktopOnWindows()) { + // For Windows with Docker Desktop installed, return the Windows path in Docker format + convertedPath = absolutePath + .replace(BACK_SLASH, FORWARD_SLASH) + .replaceFirst("${driveLetter}:/", "/${driveLetter}/").also { + log.debug("Converted path '$path' to Docker Desktop format: $it") + } + + } else if (isWslInstalled()) { + // For Windows with WSL installed, convert the path to WSL format + val windowsPath = path.toString().replace(BACK_SLASH, FORWARD_SLASH) + val wslPath = windowsPath.replaceFirst("$driveLetter:/", "/mnt/${driveLetter.lowercase()}/") + convertedPath = wslPath.also { + log.debug("Converted path '$path' to WSL format: $it") + } - if (isWindows() && isWslInstalled()) { - // For Windows with WSL installed, convert the path to WSL format - val driveLetter = path.root.toString().replace("\\", "").replace(":", "") - val windowsPath = path.toString().replace("\\", "/") - val wslPath = windowsPath.replaceFirst("$driveLetter:/", "/mnt/${driveLetter.lowercase()}/") - return wslPath + } else { + // Default case for Windows when Docker Desktop or WSL are not installed + convertedPath = absolutePath.also { + log.debug("Converted path '$path' to 'as is' format: $it") + } + } + + } else { + // Default case, return the path as-is for non-Windows OS + convertedPath = path.toAbsolutePath().toString().also { + log.debug("Converted path '$path' to 'as is' format: $it") + } } - // Default case, return the path as-is (This might be a non-Windows path or an unhandled case) - return path.toString() + return convertedPath } fun convertWindowsPathToUnix(windowsPath: String): String { - return windowsPath.replace("\\", FORWARD_SLASH).replace("^[a-zA-Z]:".toRegex(), EMPTY_STRING) + return windowsPath.replace(BACK_SLASH, FORWARD_SLASH).replace("^[a-zA-Z]:".toRegex(), EMPTY_STRING) } private fun stripNullBytes(input: String): String { diff --git a/src/test/kotlin/test/acntech/easycontainers/ContainerFileTransferTests.kt b/src/test/kotlin/test/acntech/easycontainers/ContainerFileTransferTests.kt index d2669b50..ea8a0900 100644 --- a/src/test/kotlin/test/acntech/easycontainers/ContainerFileTransferTests.kt +++ b/src/test/kotlin/test/acntech/easycontainers/ContainerFileTransferTests.kt @@ -41,6 +41,7 @@ class ContainerFileTransferTests { val content = Files.readString(path) log.debug("Content of file:$NEW_LINE$content") assertTrue(content.contains("while getopts")) + guardedExecution({container.getRuntime().delete(true)}) } @ParameterizedTest @@ -69,7 +70,7 @@ class ContainerFileTransferTests { // Call the target method container.putFile(tempFile, remoteDir, remoteFile) - TimeUnit.SECONDS.sleep(3) + TimeUnit.SECONDS.sleep(10) val path = container.getFile(remoteDir, remoteFile) @@ -79,6 +80,7 @@ class ContainerFileTransferTests { assertEquals(content, receivedContent) } finally { tempFile.deleteIfExists() + guardedExecution({runtime.delete(true)}) } } @@ -140,6 +142,7 @@ class ContainerFileTransferTests { assertEquals(content2, Files.readString(receivedFile2)) } finally { + guardedExecution({runtime.delete(true)}) guardedExecution({ tempSendDir.deleteRecursively() }) guardedExecution({ tempReceiveDir.deleteRecursively() }) } diff --git a/src/test/kotlin/test/acntech/easycontainers/ContainerTaskTests.kt b/src/test/kotlin/test/acntech/easycontainers/ContainerTaskTests.kt index dab2c1b2..b0f7e5ab 100644 --- a/src/test/kotlin/test/acntech/easycontainers/ContainerTaskTests.kt +++ b/src/test/kotlin/test/acntech/easycontainers/ContainerTaskTests.kt @@ -39,7 +39,10 @@ class ContainerTaskTests { log.debug("Container exit code: $exitCode") assertEquals(10, exitCode) - assertTrue(container.getState() == ContainerState.STOPPED || container.getState() == ContainerState.DELETED) + + assertTrue(container.getState() == ContainerState.FAILED || + container.getState() == ContainerState.STOPPED || + container.getState() == ContainerState.DELETED) container.getRuntime().delete() } diff --git a/src/test/kotlin/test/acntech/easycontainers/ContainerVolumeTests.kt b/src/test/kotlin/test/acntech/easycontainers/ContainerVolumeTests.kt new file mode 100644 index 00000000..5c9eb19f --- /dev/null +++ b/src/test/kotlin/test/acntech/easycontainers/ContainerVolumeTests.kt @@ -0,0 +1,69 @@ +package test.acntech.easycontainers + +import no.acntech.easycontainers.ContainerBuilderCallback +import no.acntech.easycontainers.model.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.slf4j.LoggerFactory +import java.io.File +import java.util.concurrent.TimeUnit +import kotlin.io.path.readText + +class ContainerVolumeTests { + + companion object { + private val log = LoggerFactory.getLogger(ContainerVolumeTests::class.java) + } + + + @ParameterizedTest + @ValueSource( + strings = [ + "DOCKER", + "KUBERNETES" + ] + ) + fun `Test mounting container file`(containerType: String) { + log.info("Testing mount container file on platform: $containerType") + + val containerPlatformType = ContainerPlatformType.valueOf(containerType) + + val content = "Hello, world!" + val mount = UnixDir.of("/tmp/test1") + val fileName = "hello.txt" + + val callback: ContainerBuilderCallback = object : ContainerBuilderCallback { + + override fun configure(builder: ContainerBuilder<*>) { + builder.withContainerFile( + ContainerFile( + ContainerFileName.of(fileName), + mount, + content, + File.createTempFile("hello", ".txt").toPath() + ) + ) + } + + } + + val container = TestSupport.startContainer( + containerPlatformType, + ExecutionMode.SERVICE, + true, + callback + ) + + log.debug("Container state: ${container.getState()}") + + val file = container.getFile(mount, fileName) + val readContent = file.readText() + assertEquals(content, readContent) + + container.getRuntime().stop() + container.getRuntime().delete() + } + +} \ No newline at end of file diff --git a/src/test/kotlin/test/acntech/easycontainers/ShowCaseTests.kt b/src/test/kotlin/test/acntech/easycontainers/ShowCaseTests.kt index 0b5db1ab..d75d8428 100644 --- a/src/test/kotlin/test/acntech/easycontainers/ShowCaseTests.kt +++ b/src/test/kotlin/test/acntech/easycontainers/ShowCaseTests.kt @@ -64,7 +64,7 @@ class ShowCaseTests { withIsEphemeral(true) - withEnv("LOG_TIME_MESSAGE", "Hello from k8s!!!") + withEnv("LOG_TIME_MESSAGE", "Hello from Kube!!!") withOutputLineCallback { line -> println("KUBERNETES-CONTAINER-OUTPUT: $line") } diff --git a/src/test/kotlin/test/acntech/easycontainers/TestSupport.kt b/src/test/kotlin/test/acntech/easycontainers/TestSupport.kt index ebb5b20e..7c852dc4 100644 --- a/src/test/kotlin/test/acntech/easycontainers/TestSupport.kt +++ b/src/test/kotlin/test/acntech/easycontainers/TestSupport.kt @@ -1,5 +1,6 @@ package test.acntech.easycontainers +import no.acntech.easycontainers.ContainerBuilderCallback import no.acntech.easycontainers.GenericContainer import no.acntech.easycontainers.docker.DockerConstants import no.acntech.easycontainers.kubernetes.K8sUtils @@ -36,6 +37,7 @@ object TestSupport { platform: ContainerPlatformType = ContainerPlatformType.DOCKER, executionMode: ExecutionMode = ExecutionMode.SERVICE, ephemeral: Boolean = true, + containerBuilderCallback: ContainerBuilderCallback? = null, ): Container { val imageName = "container-test" @@ -59,6 +61,8 @@ object TestSupport { withExecutionMode(executionMode) + withEnv("LOG_TIME_MESSAGE", "Hello from $platform running as $executionMode") + when (executionMode) { ExecutionMode.SERVICE -> { @@ -84,6 +88,9 @@ object TestSupport { withIsEphemeral(ephemeral) withOutputLineCallback { line -> println("$platform-'${imageName.uppercase()}'-CONTAINER-OUTPUT: $line") } + + containerBuilderCallback?.configure(this) + }.build() log.debug("Container created: $container") @@ -95,7 +102,7 @@ object TestSupport { Assertions.assertTrue(container.getState() == ContainerState.INITIALIZING || container.getState() == ContainerState.RUNNING) - // Wait for the container to be in the running state + // Wait for the container to reach the running state container.waitForState(ContainerState.RUNNING, 30, TimeUnit.SECONDS) Assertions.assertEquals(ContainerState.RUNNING, runtime.getContainer().getState()) diff --git a/src/test/resources/env/entry.sh b/src/test/resources/env/entrypoint.sh similarity index 100% rename from src/test/resources/env/entry.sh rename to src/test/resources/env/entrypoint.sh diff --git a/src/test/resources/env/log-time.sh b/src/test/resources/env/log-time.sh index d7de31a1..2ad1622d 100644 --- a/src/test/resources/env/log-time.sh +++ b/src/test/resources/env/log-time.sh @@ -19,15 +19,15 @@ while getopts 'es:x:i:m:' flag; do esac done -echo "Sleep time: ${sleep_time}" echo "Exit flag: ${exit_flag}" -echo "Exit code: ${exit_code}" -echo "Iterations: ${iterations}" +echo "Sleep time: ${sleep_time}" echo "Message: ${message}" count=1 if [ "${exit_flag}" -eq 1 ]; then + echo "Iterations: ${iterations}" + echo "Exit code: ${exit_code}" # Run the exit version while [ $count -le "${iterations}" ] do diff --git a/src/test/resources/env/test-dockerfile b/src/test/resources/env/test-dockerfile index 75a59f2f..c6ba76f1 100644 --- a/src/test/resources/env/test-dockerfile +++ b/src/test/resources/env/test-dockerfile @@ -26,14 +26,20 @@ COPY log-time.sh /log-time.sh RUN chmod +x /log-time.sh # Copy the entry script from the build context to the container -COPY entry.sh /entry.sh +COPY entry.sh /entrypoint.sh # Make the entry script executable -RUN chmod +x /entry.sh +RUN chmod +x /entrypoint.sh + +RUN mkdir -p /tmp/test1 +RUN chmod 777 /tmp/test1 + +RUN mkdir -p /tmp/test2 +RUN chmod 777 /tmp/test2 # Expose ports for SSH (22) and lighttpd (80) EXPOSE 22 EXPOSE 80 # Define the container's default behavior -CMD ["/entry.sh"] +CMD ["/entrypoint.sh"]