From 382249599ca1b8360eb4a96e5cf97494b1ec1a3e Mon Sep 17 00:00:00 2001 From: Nico Date: Sun, 27 Oct 2024 21:46:29 +0100 Subject: [PATCH] feat: add `cap_add` and `cap_drop` support (#726) Hi :wave: This PR implements #578 and adds tests for the added functionality. Specifically, this PR adds support for adding and dropping capabilities for containers. Have a nice weekend! --- testcontainers/src/core/containers/request.rs | 14 +++++ testcontainers/src/core/image/image_ext.rs | 26 +++++++++ testcontainers/src/runners/async_runner.rs | 56 +++++++++++++++++++ 3 files changed, 96 insertions(+) diff --git a/testcontainers/src/core/containers/request.rs b/testcontainers/src/core/containers/request.rs index bcfc69ed..81ceffaa 100644 --- a/testcontainers/src/core/containers/request.rs +++ b/testcontainers/src/core/containers/request.rs @@ -32,6 +32,8 @@ pub struct ContainerRequest { pub(crate) ports: Option>, pub(crate) ulimits: Option>, pub(crate) privileged: bool, + pub(crate) cap_add: Option>, + pub(crate) cap_drop: Option>, pub(crate) shm_size: Option, pub(crate) cgroupns_mode: Option, pub(crate) userns_mode: Option, @@ -111,6 +113,14 @@ impl ContainerRequest { self.privileged } + pub fn cap_add(&self) -> Option<&Vec> { + self.cap_add.as_ref() + } + + pub fn cap_drop(&self) -> Option<&Vec> { + self.cap_drop.as_ref() + } + pub fn cgroupns_mode(&self) -> Option { self.cgroupns_mode } @@ -187,6 +197,8 @@ impl From for ContainerRequest { ports: None, ulimits: None, privileged: false, + cap_add: None, + cap_drop: None, shm_size: None, cgroupns_mode: None, userns_mode: None, @@ -229,6 +241,8 @@ impl Debug for ContainerRequest { .field("ports", &self.ports) .field("ulimits", &self.ulimits) .field("privileged", &self.privileged) + .field("cap_add", &self.cap_add) + .field("cap_drop", &self.cap_drop) .field("shm_size", &self.shm_size) .field("cgroupns_mode", &self.cgroupns_mode) .field("userns_mode", &self.userns_mode) diff --git a/testcontainers/src/core/image/image_ext.rs b/testcontainers/src/core/image/image_ext.rs index 64aa5ddb..bdbd12af 100644 --- a/testcontainers/src/core/image/image_ext.rs +++ b/testcontainers/src/core/image/image_ext.rs @@ -90,6 +90,12 @@ pub trait ImageExt { /// Sets the container to run in privileged mode. fn with_privileged(self, privileged: bool) -> ContainerRequest; + /// Adds the capabilities to the container + fn with_cap_add(self, capability: impl Into) -> ContainerRequest; + + /// Drops the capabilities from the container's capabilities + fn with_cap_drop(self, capability: impl Into) -> ContainerRequest; + /// cgroup namespace mode for the container. Possible values are: /// - [`CgroupnsMode::Private`]: the container runs in its own private cgroup namespace /// - [`CgroupnsMode::Host`]: use the host system's cgroup namespace @@ -231,6 +237,26 @@ impl>, I: Image> ImageExt for RI { } } + fn with_cap_add(self, capability: impl Into) -> ContainerRequest { + let mut container_req = self.into(); + container_req + .cap_add + .get_or_insert_with(Vec::new) + .push(capability.into()); + + container_req + } + + fn with_cap_drop(self, capability: impl Into) -> ContainerRequest { + let mut container_req = self.into(); + container_req + .cap_drop + .get_or_insert_with(Vec::new) + .push(capability.into()); + + container_req + } + fn with_cgroupns_mode(self, cgroupns_mode: CgroupnsMode) -> ContainerRequest { let container_req = self.into(); ContainerRequest { diff --git a/testcontainers/src/runners/async_runner.rs b/testcontainers/src/runners/async_runner.rs index 401aee08..47e9942e 100644 --- a/testcontainers/src/runners/async_runner.rs +++ b/testcontainers/src/runners/async_runner.rs @@ -70,6 +70,8 @@ where extra_hosts: Some(extra_hosts), cgroupns_mode: container_req.cgroupns_mode().map(|mode| mode.into()), userns_mode: container_req.userns_mode().map(|v| v.to_string()), + cap_add: container_req.cap_add().cloned(), + cap_drop: container_req.cap_drop().cloned(), ..Default::default() }), working_dir: container_req.working_dir().map(|dir| dir.to_string()), @@ -574,6 +576,60 @@ mod tests { Ok(()) } + #[tokio::test] + async fn async_run_command_should_have_cap_add() -> anyhow::Result<()> { + let image = GenericImage::new("hello-world", "latest"); + let expected_capability = "NET_ADMIN"; + let container = image + .with_cap_add(expected_capability.to_string()) + .start() + .await?; + + let client = Client::lazy_client().await?; + let container_details = client.inspect(container.id()).await?; + + let capabilities = container_details + .host_config + .expect("HostConfig") + .cap_add + .expect("CapAdd"); + + assert_eq!( + expected_capability, + capabilities.get(0).expect("No capabilities added"), + "cap_add must contain {expected_capability}" + ); + + Ok(()) + } + + #[tokio::test] + async fn async_run_command_should_have_cap_drop() -> anyhow::Result<()> { + let image = GenericImage::new("hello-world", "latest"); + let expected_capability = "AUDIT_WRITE"; + let container = image + .with_cap_drop(expected_capability.to_string()) + .start() + .await?; + + let client = Client::lazy_client().await?; + let container_details = client.inspect(container.id()).await?; + + let capabilities = container_details + .host_config + .expect("HostConfig") + .cap_drop + .expect("CapAdd"); + + assert_eq!( + expected_capability, + capabilities.get(0).expect("No capabilities dropped"), + "cap_drop must contain {expected_capability}" + ); + + Ok(()) + } + #[tokio::test] async fn async_run_command_should_include_ulimits() -> anyhow::Result<()> { let image = GenericImage::new("hello-world", "latest");