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"]