Skip to content
This repository has been archived by the owner on Oct 22, 2023. It is now read-only.

Commit

Permalink
Add the ability to use a mounted directory as a cache instead of a Do…
Browse files Browse the repository at this point in the history
…cker volume.

See #406.
  • Loading branch information
charleskorn committed Feb 24, 2020
1 parent 1188dff commit 13a3d25
Show file tree
Hide file tree
Showing 16 changed files with 257 additions and 68 deletions.
Expand Up @@ -18,6 +18,7 @@ package batect.journeytests

import batect.journeytests.testutils.ApplicationRunner
import batect.journeytests.testutils.DockerUtils
import batect.journeytests.testutils.deleteDirectoryContents
import batect.journeytests.testutils.itCleansUpAllContainersItCreates
import batect.journeytests.testutils.itCleansUpAllNetworksItCreates
import batect.testutils.createForGroup
Expand All @@ -31,36 +32,44 @@ import org.spekframework.spek2.style.specification.describe

object CacheMountJourneyTest : Spek({
describe("running a container with a cache mounted") {
val runner by createForGroup { ApplicationRunner("cache-mount") }

beforeGroup {
DockerUtils.deleteCache("batect-cache-mount-journey-test-cache")
deleteDirectoryContents(runner.testDirectory.resolve(".batect").resolve("caches").resolve("batect-cache-mount-journey-test-cache"))
}

val runner by createForGroup { ApplicationRunner("cache-mount") }
mapOf(
"running the task with caches set to use volume mounts" to "--cache-type=volume",
"running the task with caches set to use directory mounts" to "--cache-type=directory"
).forEach { (description, arg) ->
describe(description) {
on("running the task twice") {
val firstResult by runBeforeGroup { runner.runApplication(listOf(arg, "the-task")) }
val secondResult by runBeforeGroup { runner.runApplication(listOf(arg, "the-task")) }

on("running the task twice") {
val firstResult by runBeforeGroup { runner.runApplication(listOf("the-task")) }
val secondResult by runBeforeGroup { runner.runApplication(listOf("the-task")) }
it("should not have access to the file in the cache in the first run and create it") {
assertThat(firstResult.output, containsSubstring("File does not exist, creating it\r\n"))
}

it("should not have access to the file in the cache in the first run and create it") {
assertThat(firstResult.output, containsSubstring("File does not exist, creating it\r\n"))
}
it("should have access to the file in the cache in the second run") {
assertThat(secondResult.output, containsSubstring("File exists\r\n"))
}

it("should have access to the file in the cache in the second run") {
assertThat(secondResult.output, containsSubstring("File exists\r\n"))
}
it("should succeed on the first run") {
assertThat(firstResult.exitCode, equalTo(0))
}

it("should succeed on the first run") {
assertThat(firstResult.exitCode, equalTo(0))
}
it("should succeed on the second run") {
assertThat(secondResult.exitCode, equalTo(0))
}

it("should succeed on the second run") {
assertThat(secondResult.exitCode, equalTo(0))
itCleansUpAllContainersItCreates { firstResult }
itCleansUpAllNetworksItCreates { firstResult }
itCleansUpAllContainersItCreates { secondResult }
itCleansUpAllNetworksItCreates { secondResult }
}
}

itCleansUpAllContainersItCreates { firstResult }
itCleansUpAllNetworksItCreates { firstResult }
itCleansUpAllContainersItCreates { secondResult }
itCleansUpAllNetworksItCreates { secondResult }
}
}
})
Expand Up @@ -17,6 +17,7 @@
package batect.journeytests

import batect.journeytests.testutils.ApplicationRunner
import batect.journeytests.testutils.deleteDirectoryContents
import batect.journeytests.testutils.itCleansUpAllContainersItCreates
import batect.journeytests.testutils.itCleansUpAllNetworksItCreates
import batect.testutils.createForGroup
Expand All @@ -31,7 +32,6 @@ import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
import java.io.InputStreamReader
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

object RunAsCurrentUserJourneyTest : Spek({
Expand Down Expand Up @@ -98,18 +98,6 @@ object RunAsCurrentUserJourneyTest : Spek({
}
})

private fun deleteDirectoryContents(directory: Path) {
Files.newDirectoryStream(directory).use { stream ->
stream.forEach { path ->
if (Files.isDirectory(path)) {
deleteDirectoryContents(path)
}

Files.delete(path)
}
}
}

// On Windows, all mounted directories are mounted with root as the owner and this cannot be changed.
// See https://github.com/docker/for-win/issues/63 and https://github.com/docker/for-win/issues/39.
private fun getUserNameInContainer(): String {
Expand Down
@@ -0,0 +1,36 @@
/*
Copyright 2017-2020 Charles Korn.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package batect.journeytests.testutils

import java.nio.file.Files
import java.nio.file.Path

fun deleteDirectoryContents(directory: Path) {
if (!Files.exists(directory)) {
return
}

Files.newDirectoryStream(directory).use { stream ->
stream.forEach { path ->
if (Files.isDirectory(path)) {
deleteDirectoryContents(path)
}

Files.delete(path)
}
}
}
4 changes: 3 additions & 1 deletion app/src/main/kotlin/batect/cli/CommandLineOptions.kt
Expand Up @@ -16,6 +16,7 @@

package batect.cli

import batect.execution.CacheType
import batect.execution.ConfigVariablesProvider
import batect.logging.FileLogSink
import batect.logging.LogSink
Expand Down Expand Up @@ -53,7 +54,8 @@ data class CommandLineOptions(
val dockerVerifyTLS: Boolean = false,
val dockerTlsCACertificatePath: Path = Paths.get("set-to-default-value-in", "CommandLineOptionsParser"),
val dockerTLSCertificatePath: Path = Paths.get("set-to-default-value-in", "CommandLineOptionsParser"),
val dockerTLSKeyPath: Path = Paths.get("set-to-default-value-in", "CommandLineOptionsParser")
val dockerTLSKeyPath: Path = Paths.get("set-to-default-value-in", "CommandLineOptionsParser"),
val cacheType: CacheType = CacheType.Volume
) {
fun extend(originalKodein: DKodein): DKodein = Kodein.direct {
extend(originalKodein, copy = Copy.All)
Expand Down
18 changes: 15 additions & 3 deletions app/src/main/kotlin/batect/cli/CommandLineOptionsParser.kt
Expand Up @@ -22,9 +22,11 @@ import batect.cli.options.OptionParserContainer
import batect.cli.options.OptionValueSource
import batect.cli.options.OptionsParsingResult
import batect.cli.options.ValueConverters
import batect.cli.options.defaultvalues.EnumDefaultValueProvider
import batect.cli.options.defaultvalues.EnvironmentVariableDefaultValueProviderFactory
import batect.cli.options.defaultvalues.FileDefaultValueProvider
import batect.docker.DockerHttpConfigDefaults
import batect.execution.CacheType
import batect.os.PathResolverFactory
import batect.os.SystemInfo
import batect.ui.OutputStyle
Expand Down Expand Up @@ -100,11 +102,12 @@ class CommandLineOptionsParser(
ValueConverters.pathToFile(pathResolverFactory)
)

private val requestedOutputStyle: OutputStyle? by valueOption(
private val requestedOutputStyle: OutputStyle? by valueOption<OutputStyle?, OutputStyle>(
outputOptionsGroup,
"output",
"Force a particular style of output from batect (does not affect task command output). Valid values are: fancy (default value if your console supports this), simple (no updating text), all (interleaved output from all containers) or quiet (only error messages).",
ValueConverters.optionalEnum(),
null,
ValueConverters.enum(),
'o'
)

Expand All @@ -113,6 +116,14 @@ class CommandLineOptionsParser(
private val disableCleanup: Boolean by flagOption(executionOptionsGroup, disableCleanupFlagName, "Equivalent to providing both --$disableCleanupAfterFailureFlagName and --$disableCleanupAfterSuccessFlagName.")
private val dontPropagateProxyEnvironmentVariables: Boolean by flagOption(executionOptionsGroup, "no-proxy-vars", "Don't propagate proxy-related environment variables such as http_proxy and no_proxy to image builds or containers.")

private val cacheType: CacheType by valueOption(
executionOptionsGroup,
"cache-type",
"Storage mechanism to use for caches. Valid values are: 'volume' (use Docker volumes) or 'directory' (use directories mounted from the host).",
EnumDefaultValueProvider(CacheType.Volume),
ValueConverters.enum()
)

private val dockerHost: String by valueOption(
dockerConnectionOptionsGroup,
"docker-host",
Expand Down Expand Up @@ -248,7 +259,8 @@ class CommandLineOptionsParser(
dockerVerifyTLS = dockerVerifyTLS,
dockerTLSKeyPath = resolvePathToDockerCertificate(dockerTLSKeyPath, dockerTLSKeyPathOption.valueSource, "key.pem"),
dockerTLSCertificatePath = resolvePathToDockerCertificate(dockerTLSCertificatePath, dockerTLSCertificatePathOption.valueSource, "cert.pem"),
dockerTlsCACertificatePath = resolvePathToDockerCertificate(dockerTlsCACertificatePath, dockerTLSCACertificatePathOption.valueSource, "ca.pem")
dockerTlsCACertificatePath = resolvePathToDockerCertificate(dockerTlsCACertificatePath, dockerTLSCACertificatePathOption.valueSource, "ca.pem"),
cacheType = cacheType
)
}

Expand Down
2 changes: 1 addition & 1 deletion app/src/main/kotlin/batect/cli/options/ValueConverters.kt
Expand Up @@ -51,7 +51,7 @@ object ValueConverters {
}
}

inline fun <reified T : Enum<T>> optionalEnum(): (String) -> ValueConversionResult<T?> {
inline fun <reified T : Enum<T>> enum(): (String) -> ValueConversionResult<T> {
val valueMap = enumValues<T>()
.associate { it.name.toLowerCase(Locale.ROOT) to it }

Expand Down
@@ -0,0 +1,24 @@
/*
Copyright 2017-2020 Charles Korn.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package batect.cli.options.defaultvalues

data class EnumDefaultValueProvider<T : Enum<T>>(val default: T) : DefaultValueProvider<T> {
override val value: PossibleValue<T> = PossibleValue.Valid(default)

override val description: String
get() = "Defaults to '${default.name.toLowerCase()}' if not set."
}
22 changes: 22 additions & 0 deletions app/src/main/kotlin/batect/execution/CacheType.kt
@@ -0,0 +1,22 @@
/*
Copyright 2017-2020 Charles Korn.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package batect.execution

enum class CacheType {
Volume,
Directory
}
10 changes: 8 additions & 2 deletions app/src/main/kotlin/batect/execution/VolumeMountResolver.kt
Expand Up @@ -21,6 +21,7 @@ import batect.config.ExpressionEvaluationContext
import batect.config.LiteralValue
import batect.config.ExpressionEvaluationException
import batect.config.LocalMount
import batect.config.ProjectPaths
import batect.config.VolumeMount
import batect.docker.DockerVolumeMount
import batect.docker.DockerVolumeMountSource
Expand All @@ -31,7 +32,9 @@ import batect.utils.mapToSet
class VolumeMountResolver(
private val pathResolver: PathResolver,
private val expressionEvaluationContext: ExpressionEvaluationContext,
private val cacheManager: CacheManager
private val cacheManager: CacheManager,
private val projectPaths: ProjectPaths,
private val cacheType: CacheType
) {
fun resolve(mounts: Set<VolumeMount>): Set<DockerVolumeMount> = mounts.mapToSet {
when (it) {
Expand Down Expand Up @@ -65,7 +68,10 @@ class VolumeMountResolver(
}
}

private fun resolve(mount: CacheMount): DockerVolumeMount = DockerVolumeMount(DockerVolumeMountSource.Volume("batect-cache-${cacheManager.projectCacheKey}-${mount.name}"), mount.containerPath, mount.options)
private fun resolve(mount: CacheMount): DockerVolumeMount = when (cacheType) {
CacheType.Volume -> DockerVolumeMount(DockerVolumeMountSource.Volume("batect-cache-${cacheManager.projectCacheKey}-${mount.name}"), mount.containerPath, mount.options)
CacheType.Directory -> DockerVolumeMount(DockerVolumeMountSource.LocalPath(projectPaths.cacheDirectory.resolve(mount.name).toString()), mount.containerPath, mount.options)
}
}

class VolumeMountResolutionException(message: String, cause: Throwable? = null) : RuntimeException(message, cause)
4 changes: 3 additions & 1 deletion app/src/main/kotlin/batect/ioc/InjectionConfiguration.kt
Expand Up @@ -275,7 +275,9 @@ private val executionModule = Kodein.Module("execution") {
VolumeMountResolver(
instance<PathResolverFactory>().createResolver(instance<ProjectPaths>().projectRootDirectory),
instance(),
instance()
instance(),
instance(),
commandLineOptions().cacheType
)
}
}
Expand Down
Expand Up @@ -18,6 +18,7 @@ package batect.cli

import batect.cli.options.defaultvalues.EnvironmentVariableDefaultValueProviderFactory
import batect.docker.DockerHttpConfigDefaults
import batect.execution.CacheType
import batect.os.HostEnvironmentVariables
import batect.os.PathResolutionResult
import batect.os.PathResolver
Expand Down Expand Up @@ -200,7 +201,9 @@ object CommandLineOptionsParserSpec : Spek({
dockerTLSCertificatePath = fileSystem.getPath("/resolved/some-cert"),
dockerTLSKeyPath = fileSystem.getPath("/resolved/some-key"),
taskName = "some-task"
)
),
listOf("--cache-type=volume", "some-task") to defaultCommandLineOptions.copy(cacheType = CacheType.Volume, taskName = "some-task"),
listOf("--cache-type=directory", "some-task") to defaultCommandLineOptions.copy(cacheType = CacheType.Directory, taskName = "some-task")
).forEach { (args, expectedResult) ->
given("the arguments $args") {
on("parsing the command line") {
Expand Down
Expand Up @@ -120,8 +120,8 @@ object ValueConvertersSpec : Spek({
}
}

describe("optional enum value converter") {
val converter = ValueConverters.optionalEnum<OutputStyle>()
describe("enum value converter") {
val converter = ValueConverters.enum<OutputStyle>()

given("a valid value") {
it("returns the equivalent enum constant") {
Expand Down
@@ -0,0 +1,37 @@
/*
Copyright 2017-2020 Charles Korn.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package batect.cli.options.defaultvalues

import batect.execution.CacheType
import batect.testutils.equalTo
import com.natpryce.hamkrest.assertion.assertThat
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe

object EnumDefaultValueProviderSpec : Spek({
describe("a enum default value provider") {
val provider = EnumDefaultValueProvider(CacheType.Volume)

it("provides the given value") {
assertThat(provider.value, equalTo(PossibleValue.Valid(CacheType.Volume)))
}

it("provides a description of the default value with the enum value in lowercase") {
assertThat(provider.description, equalTo("Defaults to 'volume' if not set."))
}
}
})

0 comments on commit 13a3d25

Please sign in to comment.