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

Commit

Permalink
Add support for running containers in privileged mode.
Browse files Browse the repository at this point in the history
This is required for some images such as the docker:dind image.

See also #80.
  • Loading branch information
charleskorn committed Apr 14, 2019
1 parent 0352e00 commit 767fa82
Show file tree
Hide file tree
Showing 16 changed files with 124 additions and 14 deletions.
1 change: 0 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ If there's something you're really keen to see, pull requests are always welcome
* some way to clean up old images when they're no longer needed
* allow tasks to not start any containers if they just have prerequisites (eg. pre-commit task)
* some way to reference another Dockerfile as the base image for a Dockerfile
* allow running containers in privileged mode (`--privileged`)
* allow adding and removing capabilities to containers (`--cap-add` and `--cap-drop` - https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities)
* allow giving access to a specific devices (`--device` - https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ object DockerClientIntegrationTest : Spek({
volumeMounts,
portMappings,
HealthCheckConfig(),
userAndGroup
userAndGroup,
false
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Copyright 2017-2019 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

import batect.journeytests.testutils.ApplicationRunner
import batect.journeytests.testutils.itCleansUpAllContainersItCreates
import batect.journeytests.testutils.itCleansUpAllNetworksItCreates
import batect.testutils.createForGroup
import batect.testutils.on
import batect.testutils.runBeforeGroup
import com.natpryce.hamkrest.assertion.assertThat
import com.natpryce.hamkrest.containsSubstring
import com.natpryce.hamkrest.equalTo
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe

object PrivilegedContainerTest : Spek({
describe("when running a container that requires privileged mode") {
val runner by createForGroup { ApplicationRunner("privileged-container") }

on("running a task that uses that container") {
val result by runBeforeGroup { runner.runApplication(listOf("the-task")) }

it("runs the container in privileged mode") {
assertThat(result.output, containsSubstring("Container is privileged\r\n"))
}

it("runs successfully") {
assertThat(result.exitCode, equalTo(0))
}

itCleansUpAllContainersItCreates { result }
itCleansUpAllNetworksItCreates { result }
}
}
})
15 changes: 15 additions & 0 deletions app/src/journeyTest/resources/privileged-container/batect.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
project_name: privileged-container

containers:
the-container:
image: alpine:3.8
privileged: true
volumes:
- local: .
container: /code

tasks:
the-task:
run:
container: the-container
command: /code/task.sh
15 changes: 15 additions & 0 deletions app/src/journeyTest/resources/privileged-container/task.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env sh

set -euo pipefail

# Adapted from https://stackoverflow.com/a/32144661/1668119

if ip link add dummy0 type dummy ; then
ip link delete dummy0

echo "Container is privileged"
exit 0
else
echo "Container is not privileged"
exit 1
fi
11 changes: 9 additions & 2 deletions app/src/main/kotlin/batect/config/Container.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ data class Container(
val portMappings: Set<PortMapping> = emptySet(),
val dependencies: Set<String> = emptySet(),
val healthCheckConfig: HealthCheckConfig = HealthCheckConfig(),
val runAsCurrentUserConfig: RunAsCurrentUserConfig = RunAsCurrentUserConfig.RunAsDefaultContainerUser
val runAsCurrentUserConfig: RunAsCurrentUserConfig = RunAsCurrentUserConfig.RunAsDefaultContainerUser,
val privileged: Boolean = false
) {
@Serializer(forClass = Container::class)
companion object : KSerializer<Container> {
Expand All @@ -63,6 +64,7 @@ data class Container(
private val dependenciesFieldName = "dependencies"
private val healthCheckConfigFieldName = "health_check"
private val runAsCurrentUserConfigFieldName = "run_as_current_user"
private val privilegedFieldName = "privileged"

override val descriptor: SerialDescriptor = object : SerialClassDescImpl("ContainerFromFile") {
init {
Expand All @@ -77,6 +79,7 @@ data class Container(
addElement(dependenciesFieldName, isOptional = true)
addElement(healthCheckConfigFieldName, isOptional = true)
addElement(runAsCurrentUserConfigFieldName, isOptional = true)
addElement(privilegedFieldName, isOptional = true)
}
}

Expand All @@ -91,6 +94,7 @@ data class Container(
private val dependenciesFieldIndex = descriptor.getElementIndex(dependenciesFieldName)
private val healthCheckConfigFieldIndex = descriptor.getElementIndex(healthCheckConfigFieldName)
private val runAsCurrentUserConfigFieldIndex = descriptor.getElementIndex(runAsCurrentUserConfigFieldName)
private val privilegedFieldIndex = descriptor.getElementIndex(privilegedFieldName)

override fun deserialize(decoder: Decoder): Container {
val input = decoder.beginStructure(descriptor) as YamlInput
Expand All @@ -110,6 +114,7 @@ data class Container(
var dependencies = emptySet<String>()
var healthCheckConfig = HealthCheckConfig()
var runAsCurrentUserConfig: RunAsCurrentUserConfig = RunAsCurrentUserConfig.RunAsDefaultContainerUser
var privileged = false

loop@ while (true) {
when (val i = input.decodeElementIndex(descriptor)) {
Expand All @@ -131,6 +136,7 @@ data class Container(
dependenciesFieldIndex -> dependencies = input.decode(DependencySetDeserializer)
healthCheckConfigFieldIndex -> healthCheckConfig = input.decode(HealthCheckConfig.serializer())
runAsCurrentUserConfigFieldIndex -> runAsCurrentUserConfig = input.decode(RunAsCurrentUserConfig.serializer())
privilegedFieldIndex -> privileged = input.decodeBooleanElement(descriptor, i)

else -> throw SerializationException("Unknown index $i")
}
Expand All @@ -146,7 +152,8 @@ data class Container(
portMappings,
dependencies,
healthCheckConfig,
runAsCurrentUserConfig
runAsCurrentUserConfig,
privileged
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ data class DockerContainerCreationRequest(
val volumeMounts: Set<VolumeMount>,
val portMappings: Set<PortMapping>,
val healthCheckConfig: HealthCheckConfig,
val userAndGroup: UserAndGroup?
val userAndGroup: UserAndGroup?,
val privileged: Boolean
) {
fun toJson(): String {
return json {
Expand Down Expand Up @@ -65,6 +66,7 @@ data class DockerContainerCreationRequest(
"NetworkMode" to network.id
"Binds" to formatVolumeMounts()
"PortBindings" to formatPortMappings()
"Privileged" to privileged
}
"Healthcheck" to json {
"Test" to emptyList<String>().toJsonArray()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ class DockerContainerCreationRequestFactory(
container.volumeMounts + additionalVolumeMounts,
container.portMappings + additionalPortMappings,
container.healthCheckConfig,
userAndGroup
userAndGroup,
container.privileged
)
}

Expand Down
2 changes: 2 additions & 0 deletions app/src/unitTest/kotlin/batect/config/ContainerSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ object ContainerSpec : Spek({
run_as_current_user:
enabled: true
home_directory: /home/something
privileged: true
""".trimIndent()

on("loading the configuration from the config file") {
Expand Down Expand Up @@ -233,6 +234,7 @@ object ContainerSpec : Spek({
assertThat(result.portMappings, equalTo(setOf(PortMapping(1234, 5678), PortMapping(9012, 3456))))
assertThat(result.healthCheckConfig, equalTo(HealthCheckConfig(Duration.ofSeconds(2), 10, Duration.ofSeconds(1))))
assertThat(result.runAsCurrentUserConfig, equalTo(RunAsCurrentUserConfig.RunAsCurrentUser("/home/something")))
assertThat(result.privileged, equalTo(true))
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/unitTest/kotlin/batect/docker/DockerAPISpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ object DockerAPISpec : Spek({
val image = DockerImage("the-image")
val network = DockerNetwork("the-network")
val command = listOf("doStuff")
val request = DockerContainerCreationRequest(image, network, command, "some-host", "some-host", emptyMap(), "/some-dir", emptySet(), emptySet(), HealthCheckConfig(), null)
val request = DockerContainerCreationRequest(image, network, command, "some-host", "some-host", emptyMap(), "/some-dir", emptySet(), emptySet(), HealthCheckConfig(), null, false)

on("a successful creation") {
val call by createForEachTest { httpClient.mockPost(expectedUrl, """{"Id": "abc123"}""", 201) }
Expand Down
2 changes: 1 addition & 1 deletion app/src/unitTest/kotlin/batect/docker/DockerClientSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ object DockerClientSpec : Spek({
val image = DockerImage("the-image")
val network = DockerNetwork("the-network")
val command = listOf("doStuff")
val request = DockerContainerCreationRequest(image, network, command, "some-host", "some-host", emptyMap(), "/some-dir", emptySet(), emptySet(), HealthCheckConfig(), null)
val request = DockerContainerCreationRequest(image, network, command, "some-host", "some-host", emptyMap(), "/some-dir", emptySet(), emptySet(), HealthCheckConfig(), null, false)

on("creating the container") {
beforeEachTest { whenever(api.createContainer(request)).doReturn(DockerContainer("abc123")) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ object DockerContainerCreationRequestFactorySpec : Spek({
volumeMounts = setOf(VolumeMount("local", "remote", "mode")),
portMappings = setOf(PortMapping(123, 456)),
environment = mapOf("SOME_VAR" to LiteralValue("SOME_VALUE")),
healthCheckConfig = HealthCheckConfig(Duration.ofSeconds(2), 10, Duration.ofSeconds(5))
healthCheckConfig = HealthCheckConfig(Duration.ofSeconds(2), 10, Duration.ofSeconds(5)),
privileged = false
)

val userAndGroup = UserAndGroup(123, 456)
Expand Down Expand Up @@ -127,6 +128,10 @@ object DockerContainerCreationRequestFactorySpec : Spek({
it("populates the user and group configuration on the request with the provided values") {
assertThat(request.userAndGroup, equalTo(userAndGroup))
}

it("populates the privileged mode with the setting from the container") {
assertThat(request.privileged, equalTo(false))
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ object DockerContainerCreationRequestSpec : Spek({
setOf(VolumeMount("/local", "/container", "ro")),
setOf(PortMapping(123, 456)),
HealthCheckConfig(Duration.ofNanos(555), 12, Duration.ofNanos(333)),
UserAndGroup(789, 222)
UserAndGroup(789, 222),
true
)

on("converting it to JSON") {
Expand Down Expand Up @@ -78,7 +79,8 @@ object DockerContainerCreationRequestSpec : Spek({
| "HostPort": "123"
| }
| ]
| }
| },
| "Privileged": true
| },
| "Healthcheck": {
| "Test": [],
Expand Down Expand Up @@ -112,7 +114,8 @@ object DockerContainerCreationRequestSpec : Spek({
emptySet(),
emptySet(),
HealthCheckConfig(),
null
null,
false
)

on("converting it to JSON") {
Expand All @@ -133,7 +136,8 @@ object DockerContainerCreationRequestSpec : Spek({
| "HostConfig": {
| "NetworkMode": "the-network",
| "Binds": [],
| "PortBindings": {}
| "PortBindings": {},
| "Privileged": false
| },
| "Healthcheck": {
| "Test": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ object TaskStepRunnerSpec : Spek({
val network = DockerNetwork("some-network")

val step = CreateContainerStep(container, command, workingDirectory, additionalEnvironmentVariables, additionalPortMappings, setOf(container, otherContainer), image, network)
val request = DockerContainerCreationRequest(image, network, command.parsedCommand, "some-container", "some-container", emptyMap(), "/work-dir", emptySet(), emptySet(), HealthCheckConfig(), null)
val request = DockerContainerCreationRequest(image, network, command.parsedCommand, "some-container", "some-container", emptyMap(), "/work-dir", emptySet(), emptySet(), HealthCheckConfig(), null, false)

beforeEachTest {
whenever(
Expand Down
5 changes: 5 additions & 0 deletions docs/content/config/Containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ by that user, so this is less of an issue. However, for consistency, the same co

See [this page](../tips/BuildArtifactsOwnedByRoot.md) for more information on the effects of this option and why it is necessary.

## `privileged`
Run the container in [privileged mode](https://docs.docker.com/engine/reference/commandline/run/#full-container-capabilities---privileged).

Available since v0.29.

## Examples

For more examples and real-world scenarios, take a look at the [sample projects](../SampleProjects.md).
Expand Down
4 changes: 4 additions & 0 deletions tools/schema/configSchema.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@
},
"run_as_current_user": {
"$ref": "#/definitions/runAsCurrentUserOptions"
},
"privileged": {
"type": "boolean",
"description": "Enable privileged mode for the container"
}
},
"oneOf": [
Expand Down

0 comments on commit 767fa82

Please sign in to comment.