Skip to content

Commit

Permalink
refactor!: add ImageExt trait to avoid explicit conversion to `Runn…
Browse files Browse the repository at this point in the history
…ableImage` (#652)

This PR allows to override some image values without explicit conversion
to `RunnableImage`.

For example:

```rs
// Instead of this:
use testcontainers::RunnableImage;
RunnableImage::from(image).with_tag("x").with_network("a").start();

// It can be done directly on image:
use testcontainers::ImageExt;
image.with_tag("x").with_network("a").start();
```

Also, it allows to simplify `GenericImage`. Because it was kinda
duplicative code.
  • Loading branch information
DDtKey authored Jun 12, 2024
1 parent 4b3d2d8 commit 2b9b4d4
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 287 deletions.
7 changes: 4 additions & 3 deletions docs/quickstart/community_modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,19 @@ for more details.
## 2. How to override module defaults
Sometimes it's necessary to override default settings of the module (e.g `tag`, `name`, environment variables etc.)
In order to do that, just use [RunnableImage](https://docs.rs/testcontainers/latest/testcontainers/core/struct.RunnableImage.html):
In order to do that, just use extension trait [ImageExt](https://docs.rs/testcontainers/latest/testcontainers/core/trait.ImageExt.html)
that returns customized [RunnableImage](https://docs.rs/testcontainers/latest/testcontainers/core/struct.RunnableImage.html):
```rust
use testcontainers_modules::{
redis::Redis,
testcontainers::RunnableImage
testcontainers::{RunnableImage, ImageExt},
};
/// Create a Redis module with `6.2-alpine` tag and custom password
fn create_redis() -> RunnableImage<Redis> {
RunnableImage::from(Redis::default())
Redis::default()
.with_tag("6.2-alpine")
.with_env_var(("REDIS_PASSWORD", "my_secret_password"))
}
Expand Down
2 changes: 1 addition & 1 deletion testcontainers/src/core.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
pub use self::{
containers::*,
image::{
CgroupnsMode, CmdWaitFor, ContainerState, ExecCommand, Host, Image, PortMapping,
CgroupnsMode, CmdWaitFor, ContainerState, ExecCommand, Host, Image, ImageExt, PortMapping,
RunnableImage, WaitFor,
},
mounts::{AccessMode, Mount, MountType},
Expand Down
3 changes: 1 addition & 2 deletions testcontainers/src/core/containers/async_container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,13 +392,12 @@ mod tests {

use tokio::io::AsyncBufReadExt;

use super::*;
use crate::{images::generic::GenericImage, runners::AsyncRunner};

#[tokio::test]
async fn async_logs_are_accessible() -> anyhow::Result<()> {
let image = GenericImage::new("testcontainers/helloworld", "1.1.0");
let container = RunnableImage::from(image).start().await?;
let container = image.start().await?;

let stderr = container.stderr(true);

Expand Down
4 changes: 2 additions & 2 deletions testcontainers/src/core/containers/sync_container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ impl<I: Image> Drop for Container<I> {
#[cfg(test)]
mod test {
use super::*;
use crate::{core::WaitFor, runners::SyncRunner, GenericImage, RunnableImage};
use crate::{core::WaitFor, runners::SyncRunner, GenericImage};

#[derive(Debug, Default)]
pub struct HelloWorld;
Expand Down Expand Up @@ -238,7 +238,7 @@ mod test {
#[test]
fn sync_logs_are_accessible() -> anyhow::Result<()> {
let image = GenericImage::new("testcontainers/helloworld", "1.1.0");
let container = RunnableImage::from(image).start()?;
let container = image.start()?;

let stderr = container.stderr(true);

Expand Down
2 changes: 2 additions & 0 deletions testcontainers/src/core/image.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use std::{borrow::Cow, fmt::Debug};

pub use exec::{CmdWaitFor, ExecCommand};
pub use image_ext::ImageExt;
pub use runnable_image::{CgroupnsMode, Host, PortMapping, RunnableImage};
pub use wait_for::WaitFor;

use super::ports::Ports;
use crate::{core::mounts::Mount, TestcontainersError};

mod exec;
mod image_ext;
mod runnable_image;
mod wait_for;

Expand Down
184 changes: 184 additions & 0 deletions testcontainers/src/core/image/image_ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
use std::time::Duration;

use crate::{
core::{CgroupnsMode, Host, Mount, PortMapping},
Image, RunnableImage,
};

pub trait ImageExt<I: Image> {
/// Returns a new RunnableImage with the specified (overridden) `CMD` ([`Image::cmd`]).
///
/// # Examples
/// ```rust,no_run
/// use testcontainers::{GenericImage, ImageExt};
///
/// let image = GenericImage::new("image", "tag");
/// let cmd = ["arg1", "arg2"];
/// let overridden_cmd = image.clone().with_cmd(cmd);
///
/// assert!(overridden_cmd.cmd().eq(cmd));
///
/// let another_runnable_image = image.with_cmd(cmd);
///
/// assert!(another_runnable_image.cmd().eq(overridden_cmd.cmd()));
/// ```
fn with_cmd(self, cmd: impl IntoIterator<Item = impl Into<String>>) -> RunnableImage<I>;

/// Overrides the fully qualified image name (consists of `{domain}/{owner}/{image}`).
/// Can be used to specify a custom registry or owner.
fn with_name(self, name: impl Into<String>) -> RunnableImage<I>;

/// Overrides the image tag.
///
/// There is no guarantee that the specified tag for an image would result in a
/// running container. Users of this API are advised to use this at their own risk.
fn with_tag(self, tag: impl Into<String>) -> RunnableImage<I>;

/// Sets the container name.
fn with_container_name(self, name: impl Into<String>) -> RunnableImage<I>;

/// Sets the network the container will be connected to.
fn with_network(self, network: impl Into<String>) -> RunnableImage<I>;

/// Adds an environment variable to the container.
fn with_env_var(self, name: impl Into<String>, value: impl Into<String>) -> RunnableImage<I>;

/// Adds a host to the container.
fn with_host(self, key: impl Into<String>, value: impl Into<Host>) -> RunnableImage<I>;

/// Adds a mount to the container.
fn with_mount(self, mount: impl Into<Mount>) -> RunnableImage<I>;

/// Adds a port mapping to the container.
fn with_mapped_port<P: Into<PortMapping>>(self, port: P) -> RunnableImage<I>;

/// Sets the container to run in privileged mode.
fn with_privileged(self, privileged: bool) -> RunnableImage<I>;

/// 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
/// If not specified, the daemon default is used, which can either be `\"private\"` or `\"host\"`, depending on daemon version, kernel support and configuration.

Check warning on line 61 in testcontainers/src/core/image/image_ext.rs

View workflow job for this annotation

GitHub Actions / clippy

doc list item missing indentation

warning: doc list item missing indentation --> testcontainers/src/core/image/image_ext.rs:61:9 | 61 | /// If not specified, the daemon default is used, which can either be `\"private\"` or `\"host\"`, depending on daemon version, kerne... | ^ | = help: if this is supposed to be its own paragraph, add a blank line = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#doc_lazy_continuation = note: `#[warn(clippy::doc_lazy_continuation)]` on by default help: indent this line | 61 | /// If not specified, the daemon default is used, which can either be `\"private\"` or `\"host\"`, depending on daemon version, kernel support and configuration. | ++
fn with_cgroupns_mode(self, cgroupns_mode: CgroupnsMode) -> RunnableImage<I>;

/// Sets the usernamespace mode for the container when usernamespace remapping option is enabled.
fn with_userns_mode(self, userns_mode: &str) -> RunnableImage<I>;

/// Sets the shared memory size in bytes
fn with_shm_size(self, bytes: u64) -> RunnableImage<I>;

/// Sets the startup timeout for the container. The default is 60 seconds.
fn with_startup_timeout(self, timeout: Duration) -> RunnableImage<I>;
}

impl<RI: Into<RunnableImage<I>>, I: Image> ImageExt<I> for RI {
fn with_cmd(self, cmd: impl IntoIterator<Item = impl Into<String>>) -> RunnableImage<I> {
let runnable = self.into();
RunnableImage {
overridden_cmd: cmd.into_iter().map(Into::into).collect(),
..runnable
}
}

fn with_name(self, name: impl Into<String>) -> RunnableImage<I> {
let runnable = self.into();
RunnableImage {
image_name: Some(name.into()),
..runnable
}
}

fn with_tag(self, tag: impl Into<String>) -> RunnableImage<I> {
let runnable = self.into();
RunnableImage {
image_tag: Some(tag.into()),
..runnable
}
}

fn with_container_name(self, name: impl Into<String>) -> RunnableImage<I> {
let runnable = self.into();

RunnableImage {
container_name: Some(name.into()),
..runnable
}
}

fn with_network(self, network: impl Into<String>) -> RunnableImage<I> {
let runnable = self.into();
RunnableImage {
network: Some(network.into()),
..runnable
}
}

fn with_env_var(self, name: impl Into<String>, value: impl Into<String>) -> RunnableImage<I> {
let mut runnable = self.into();
runnable.env_vars.insert(name.into(), value.into());
runnable
}

fn with_host(self, key: impl Into<String>, value: impl Into<Host>) -> RunnableImage<I> {
let mut runnable = self.into();
runnable.hosts.insert(key.into(), value.into());
runnable
}

fn with_mount(self, mount: impl Into<Mount>) -> RunnableImage<I> {
let mut runnable = self.into();
runnable.mounts.push(mount.into());
runnable
}

fn with_mapped_port<P: Into<PortMapping>>(self, port: P) -> RunnableImage<I> {
let runnable = self.into();
let mut ports = runnable.ports.unwrap_or_default();
ports.push(port.into());

RunnableImage {
ports: Some(ports),
..runnable
}
}

fn with_privileged(self, privileged: bool) -> RunnableImage<I> {
let runnable = self.into();
RunnableImage {
privileged,
..runnable
}
}

fn with_cgroupns_mode(self, cgroupns_mode: CgroupnsMode) -> RunnableImage<I> {
let runnable = self.into();
RunnableImage {
cgroupns_mode: Some(cgroupns_mode),
..runnable
}
}

fn with_userns_mode(self, userns_mode: &str) -> RunnableImage<I> {
let runnable = self.into();
RunnableImage {
userns_mode: Some(String::from(userns_mode)),
..runnable
}
}

fn with_shm_size(self, bytes: u64) -> RunnableImage<I> {
let runnable = self.into();
RunnableImage {
shm_size: Some(bytes),
..runnable
}
}

fn with_startup_timeout(self, timeout: Duration) -> RunnableImage<I> {
let runnable = self.into();
RunnableImage {
startup_timeout: Some(timeout),
..runnable
}
}
}
Loading

0 comments on commit 2b9b4d4

Please sign in to comment.