Skip to content

Commit 3822495

Browse files
authored
feat: add cap_add and cap_drop support (#726)
Hi 👋 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!
1 parent 7711714 commit 3822495

3 files changed

Lines changed: 96 additions & 0 deletions

File tree

testcontainers/src/core/containers/request.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ pub struct ContainerRequest<I: Image> {
3232
pub(crate) ports: Option<Vec<PortMapping>>,
3333
pub(crate) ulimits: Option<Vec<ResourcesUlimits>>,
3434
pub(crate) privileged: bool,
35+
pub(crate) cap_add: Option<Vec<String>>,
36+
pub(crate) cap_drop: Option<Vec<String>>,
3537
pub(crate) shm_size: Option<u64>,
3638
pub(crate) cgroupns_mode: Option<CgroupnsMode>,
3739
pub(crate) userns_mode: Option<String>,
@@ -111,6 +113,14 @@ impl<I: Image> ContainerRequest<I> {
111113
self.privileged
112114
}
113115

116+
pub fn cap_add(&self) -> Option<&Vec<String>> {
117+
self.cap_add.as_ref()
118+
}
119+
120+
pub fn cap_drop(&self) -> Option<&Vec<String>> {
121+
self.cap_drop.as_ref()
122+
}
123+
114124
pub fn cgroupns_mode(&self) -> Option<CgroupnsMode> {
115125
self.cgroupns_mode
116126
}
@@ -187,6 +197,8 @@ impl<I: Image> From<I> for ContainerRequest<I> {
187197
ports: None,
188198
ulimits: None,
189199
privileged: false,
200+
cap_add: None,
201+
cap_drop: None,
190202
shm_size: None,
191203
cgroupns_mode: None,
192204
userns_mode: None,
@@ -229,6 +241,8 @@ impl<I: Image + Debug> Debug for ContainerRequest<I> {
229241
.field("ports", &self.ports)
230242
.field("ulimits", &self.ulimits)
231243
.field("privileged", &self.privileged)
244+
.field("cap_add", &self.cap_add)
245+
.field("cap_drop", &self.cap_drop)
232246
.field("shm_size", &self.shm_size)
233247
.field("cgroupns_mode", &self.cgroupns_mode)
234248
.field("userns_mode", &self.userns_mode)

testcontainers/src/core/image/image_ext.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ pub trait ImageExt<I: Image> {
9090
/// Sets the container to run in privileged mode.
9191
fn with_privileged(self, privileged: bool) -> ContainerRequest<I>;
9292

93+
/// Adds the capabilities to the container
94+
fn with_cap_add(self, capability: impl Into<String>) -> ContainerRequest<I>;
95+
96+
/// Drops the capabilities from the container's capabilities
97+
fn with_cap_drop(self, capability: impl Into<String>) -> ContainerRequest<I>;
98+
9399
/// cgroup namespace mode for the container. Possible values are:
94100
/// - [`CgroupnsMode::Private`]: the container runs in its own private cgroup namespace
95101
/// - [`CgroupnsMode::Host`]: use the host system's cgroup namespace
@@ -231,6 +237,26 @@ impl<RI: Into<ContainerRequest<I>>, I: Image> ImageExt<I> for RI {
231237
}
232238
}
233239

240+
fn with_cap_add(self, capability: impl Into<String>) -> ContainerRequest<I> {
241+
let mut container_req = self.into();
242+
container_req
243+
.cap_add
244+
.get_or_insert_with(Vec::new)
245+
.push(capability.into());
246+
247+
container_req
248+
}
249+
250+
fn with_cap_drop(self, capability: impl Into<String>) -> ContainerRequest<I> {
251+
let mut container_req = self.into();
252+
container_req
253+
.cap_drop
254+
.get_or_insert_with(Vec::new)
255+
.push(capability.into());
256+
257+
container_req
258+
}
259+
234260
fn with_cgroupns_mode(self, cgroupns_mode: CgroupnsMode) -> ContainerRequest<I> {
235261
let container_req = self.into();
236262
ContainerRequest {

testcontainers/src/runners/async_runner.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ where
7070
extra_hosts: Some(extra_hosts),
7171
cgroupns_mode: container_req.cgroupns_mode().map(|mode| mode.into()),
7272
userns_mode: container_req.userns_mode().map(|v| v.to_string()),
73+
cap_add: container_req.cap_add().cloned(),
74+
cap_drop: container_req.cap_drop().cloned(),
7375
..Default::default()
7476
}),
7577
working_dir: container_req.working_dir().map(|dir| dir.to_string()),
@@ -574,6 +576,60 @@ mod tests {
574576
Ok(())
575577
}
576578

579+
#[tokio::test]
580+
async fn async_run_command_should_have_cap_add() -> anyhow::Result<()> {
581+
let image = GenericImage::new("hello-world", "latest");
582+
let expected_capability = "NET_ADMIN";
583+
let container = image
584+
.with_cap_add(expected_capability.to_string())
585+
.start()
586+
.await?;
587+
588+
let client = Client::lazy_client().await?;
589+
let container_details = client.inspect(container.id()).await?;
590+
591+
let capabilities = container_details
592+
.host_config
593+
.expect("HostConfig")
594+
.cap_add
595+
.expect("CapAdd");
596+
597+
assert_eq!(
598+
expected_capability,
599+
capabilities.get(0).expect("No capabilities added"),
600+
"cap_add must contain {expected_capability}"
601+
);
602+
603+
Ok(())
604+
}
605+
606+
#[tokio::test]
607+
async fn async_run_command_should_have_cap_drop() -> anyhow::Result<()> {
608+
let image = GenericImage::new("hello-world", "latest");
609+
let expected_capability = "AUDIT_WRITE";
610+
let container = image
611+
.with_cap_drop(expected_capability.to_string())
612+
.start()
613+
.await?;
614+
615+
let client = Client::lazy_client().await?;
616+
let container_details = client.inspect(container.id()).await?;
617+
618+
let capabilities = container_details
619+
.host_config
620+
.expect("HostConfig")
621+
.cap_drop
622+
.expect("CapAdd");
623+
624+
assert_eq!(
625+
expected_capability,
626+
capabilities.get(0).expect("No capabilities dropped"),
627+
"cap_drop must contain {expected_capability}"
628+
);
629+
630+
Ok(())
631+
}
632+
577633
#[tokio::test]
578634
async fn async_run_command_should_include_ulimits() -> anyhow::Result<()> {
579635
let image = GenericImage::new("hello-world", "latest");

0 commit comments

Comments
 (0)