Skip to content

Commit

Permalink
feat: add support for user-configured labels (#756)
Browse files Browse the repository at this point in the history
PR adds support for applying user-configured labels to containers
started by `testcontainers`.

> [!NOTE]
> This PR is a precursor to future support of [reusable
containers](#742)
  • Loading branch information
the-wondersmith authored Dec 6, 2024
1 parent 2f2dca1 commit 2756f68
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 3 deletions.
7 changes: 7 additions & 0 deletions testcontainers/src/core/containers/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub struct ContainerRequest<I: Image> {
pub(crate) image_tag: Option<String>,
pub(crate) container_name: Option<String>,
pub(crate) network: Option<String>,
pub(crate) labels: BTreeMap<String, String>,
pub(crate) env_vars: BTreeMap<String, String>,
pub(crate) hosts: BTreeMap<String, Host>,
pub(crate) mounts: Vec<Mount>,
Expand Down Expand Up @@ -74,6 +75,10 @@ impl<I: Image> ContainerRequest<I> {
&self.network
}

pub fn labels(&self) -> &BTreeMap<String, String> {
&self.labels
}

pub fn container_name(&self) -> &Option<String> {
&self.container_name
}
Expand Down Expand Up @@ -190,6 +195,7 @@ impl<I: Image> From<I> for ContainerRequest<I> {
image_tag: None,
container_name: None,
network: None,
labels: BTreeMap::default(),
env_vars: BTreeMap::default(),
hosts: BTreeMap::default(),
mounts: Vec::new(),
Expand Down Expand Up @@ -235,6 +241,7 @@ impl<I: Image + Debug> Debug for ContainerRequest<I> {
.field("image_tag", &self.image_tag)
.field("container_name", &self.container_name)
.field("network", &self.network)
.field("labels", &self.labels)
.field("env_vars", &self.env_vars)
.field("hosts", &self.hosts)
.field("mounts", &self.mounts)
Expand Down
40 changes: 40 additions & 0 deletions testcontainers/src/core/image/image_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,23 @@ pub trait ImageExt<I: Image> {
/// Sets the network the container will be connected to.
fn with_network(self, network: impl Into<String>) -> ContainerRequest<I>;

/// Adds the specified label to the container.
///
/// **Note**: all keys in the `org.testcontainers.*` namespace should be regarded
/// as reserved by `testcontainers` internally, and should not be expected or relied
/// upon to be applied correctly if supplied as a value for `key`.
fn with_label(self, key: impl Into<String>, value: impl Into<String>) -> ContainerRequest<I>;

/// Adds the specified labels to the container.
///
/// **Note**: all keys in the `org.testcontainers.*` namespace should be regarded
/// as reserved by `testcontainers` internally, and should not be expected or relied
/// upon to be applied correctly if they are included in `labels`.
fn with_labels(
self,
labels: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
) -> ContainerRequest<I>;

/// Adds an environment variable to the container.
fn with_env_var(self, name: impl Into<String>, value: impl Into<String>)
-> ContainerRequest<I>;
Expand Down Expand Up @@ -164,6 +181,29 @@ impl<RI: Into<ContainerRequest<I>>, I: Image> ImageExt<I> for RI {
}
}

fn with_label(self, key: impl Into<String>, value: impl Into<String>) -> ContainerRequest<I> {
let mut container_req = self.into();

container_req.labels.insert(key.into(), value.into());

container_req
}

fn with_labels(
self,
labels: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
) -> ContainerRequest<I> {
let mut container_req = self.into();

container_req.labels.extend(
labels
.into_iter()
.map(|(key, value)| (key.into(), value.into())),
);

container_req
}

fn with_env_var(
self,
name: impl Into<String>,
Expand Down
63 changes: 60 additions & 3 deletions testcontainers/src/runners/async_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,20 @@ where
.map(|(key, value)| format!("{key}:{value}"))
.collect();

let labels = HashMap::<String, String>::from_iter(
container_req
.labels()
.iter()
.map(|(key, value)| (key.into(), value.into()))
.chain([(
"org.testcontainers.managed-by".into(),
"testcontainers".into(),
)]),
);

let mut config: Config<String> = Config {
image: Some(container_req.descriptor()),
labels: Some(labels),
host_config: Some(HostConfig {
privileged: Some(container_req.privileged()),
extra_hosts: Some(extra_hosts),
Expand Down Expand Up @@ -297,6 +309,51 @@ mod tests {
ImageExt,
};

/// Test that all user-supplied labels are added to containers started by `AsyncRunner::start`
#[tokio::test]
async fn async_start_should_apply_expected_labels() -> anyhow::Result<()> {
let mut labels = HashMap::from([
("foo".to_string(), "bar".to_string()),
("baz".to_string(), "qux".to_string()),
(
"org.testcontainers.managed-by".to_string(),
"the-time-wizard".to_string(),
),
]);

let container = GenericImage::new("hello-world", "latest")
.with_labels(&labels)
.start()
.await?;

let client = Client::lazy_client().await?;

let container_labels = client
.inspect(container.id())
.await?
.config
.unwrap_or_default()
.labels
.unwrap_or_default();

// the created labels and container labels shouldn't actually be identical, as the
// `org.testcontainers.managed-by: testcontainers` label is always unconditionally
// applied to all containers by `AsyncRunner::start`, with the value `testcontainers`
// being applied *last* explicitly so that even user-supplied values of the
// `org.testcontainers.managed-by` key will be overwritten
assert_ne!(&labels, &container_labels);

// If we add the expected `managed-by` value though, they should then match
labels.insert(
"org.testcontainers.managed-by".to_string(),
"testcontainers".to_string(),
);

assert_eq!(labels, container_labels);

Ok(())
}

#[tokio::test]
async fn async_run_command_should_expose_all_ports_if_no_explicit_mapping_requested(
) -> anyhow::Result<()> {
Expand Down Expand Up @@ -500,7 +557,7 @@ mod tests {
}

// containers have been dropped, should clean up networks
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
tokio::time::sleep(Duration::from_secs(1)).await;
let client = Client::lazy_client().await?;
assert!(!client.network_exists("awesome-net-2").await?);
Ok(())
Expand Down Expand Up @@ -596,7 +653,7 @@ mod tests {

assert_eq!(
expected_capability,
capabilities.get(0).expect("No capabilities added"),
capabilities.first().expect("No capabilities added"),
"cap_add must contain {expected_capability}"
);

Expand All @@ -623,7 +680,7 @@ mod tests {

assert_eq!(
expected_capability,
capabilities.get(0).expect("No capabilities dropped"),
capabilities.first().expect("No capabilities dropped"),
"cap_drop must contain {expected_capability}"
);

Expand Down

0 comments on commit 2756f68

Please sign in to comment.