From cb014a089fdd12d9d2a68e75dfdb33be10635298 Mon Sep 17 00:00:00 2001 From: Muhammad Mominul Huque Date: Fri, 5 Sep 2025 20:10:27 +0600 Subject: [PATCH 1/3] Expose container's `platform` config in ImageExt trait --- testcontainers/src/core/containers/request.rs | 7 ++++++ testcontainers/src/core/image/image_ext.rs | 23 ++++++++++++++++++ testcontainers/src/runners/async_runner.rs | 24 +++++++++++++++---- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/testcontainers/src/core/containers/request.rs b/testcontainers/src/core/containers/request.rs index 378357a3..fb264df5 100644 --- a/testcontainers/src/core/containers/request.rs +++ b/testcontainers/src/core/containers/request.rs @@ -26,6 +26,7 @@ pub struct ContainerRequest { pub(crate) image_name: Option, pub(crate) image_tag: Option, pub(crate) container_name: Option, + pub(crate) platform: Option, pub(crate) network: Option, pub(crate) labels: BTreeMap, pub(crate) env_vars: BTreeMap, @@ -94,6 +95,10 @@ impl ContainerRequest { &self.container_name } + pub fn platform(&self) -> &Option { + &self.platform + } + pub fn env_vars(&self) -> impl Iterator, Cow<'_, str>)> { self.image .env_vars() @@ -236,6 +241,7 @@ impl From for ContainerRequest { image_name: None, image_tag: None, container_name: None, + platform: None, network: None, labels: BTreeMap::default(), env_vars: BTreeMap::default(), @@ -292,6 +298,7 @@ impl Debug for ContainerRequest { .field("image_name", &self.image_name) .field("image_tag", &self.image_tag) .field("container_name", &self.container_name) + .field("platform", &self.platform) .field("network", &self.network) .field("labels", &self.labels) .field("env_vars", &self.env_vars) diff --git a/testcontainers/src/core/image/image_ext.rs b/testcontainers/src/core/image/image_ext.rs index 80db0644..b98cab6a 100644 --- a/testcontainers/src/core/image/image_ext.rs +++ b/testcontainers/src/core/image/image_ext.rs @@ -68,6 +68,20 @@ pub trait ImageExt { /// Sets the container name. fn with_container_name(self, name: impl Into) -> ContainerRequest; + /// Sets the platform the container will be run on. + /// + /// Platform in the format os[/arch[/variant]] used for image lookup. + /// + /// # Examples + /// + /// ```rust,no_run + /// use testcontainers::{GenericImage, ImageExt}; + /// + /// let image = GenericImage::new("image", "tag") + /// .with_platform("linux/amd64"); + /// ``` + fn with_platform(self, platform: impl Into) -> ContainerRequest; + /// Sets the network the container will be connected to. fn with_network(self, network: impl Into) -> ContainerRequest; @@ -272,6 +286,15 @@ impl>, I: Image> ImageExt for RI { } } + fn with_platform(self, platform: impl Into) -> ContainerRequest { + let container_req = self.into(); + + ContainerRequest { + platform: Some(platform.into()), + ..container_req + } + } + fn with_network(self, network: impl Into) -> ContainerRequest { let container_req = self.into(); ContainerRequest { diff --git a/testcontainers/src/runners/async_runner.rs b/testcontainers/src/runners/async_runner.rs index 360b88b4..54c2b2d5 100644 --- a/testcontainers/src/runners/async_runner.rs +++ b/testcontainers/src/runners/async_runner.rs @@ -177,13 +177,27 @@ where None }; + let mut options_builder: Option = None; + // name of the container if let Some(name) = container_req.container_name() { - let options = CreateContainerOptionsBuilder::new() - .name(name) - .platform(client.config.platform().unwrap_or_default()) - .build(); - create_options = Some(options) + let options = CreateContainerOptionsBuilder::new().name(name); + + options_builder = Some(options); + } + + // platform of the container + if let Some(platform) = container_req.platform() { + let options = options_builder.unwrap_or(CreateContainerOptionsBuilder::new()); + options_builder = Some(options.platform(platform)); + } else { + // set platform from global platform setting if available + let options = options_builder.unwrap_or(CreateContainerOptionsBuilder::new()); + options_builder = Some(options.platform(client.config.platform().unwrap_or_default())); + } + + if let Some(options) = options_builder { + create_options = Some(options.build()); } // handle environment variables From 2e01041a1e7ebba18aaeed1b2b7e76f04a1023f0 Mon Sep 17 00:00:00 2001 From: Muhammad Mominul Huque Date: Sat, 6 Sep 2025 00:55:06 +0600 Subject: [PATCH 2/3] Pass the platform config when pulling a image --- testcontainers/src/core/client.rs | 19 ++++++++++++++----- testcontainers/src/core/image/image_ext.rs | 2 +- testcontainers/src/runners/async_runner.rs | 14 ++++++++++++-- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/testcontainers/src/core/client.rs b/testcontainers/src/core/client.rs index b66c8b82..311ca7a0 100644 --- a/testcontainers/src/core/client.rs +++ b/testcontainers/src/core/client.rs @@ -445,10 +445,19 @@ impl Client { Ok(()) } - pub(crate) async fn pull_image(&self, descriptor: &str) -> Result<(), ClientError> { + pub(crate) async fn pull_image( + &self, + descriptor: &str, + platform: Option, + ) -> Result<(), ClientError> { let pull_options = CreateImageOptionsBuilder::new() .from_image(descriptor) - .platform(self.config.platform().unwrap_or_default()) + .platform( + platform + .as_ref() + .map(|s| s.as_str()) + .unwrap_or_else(|| self.config.platform().unwrap_or_default()), + ) .build(); let credentials = self.credentials_for_image(descriptor).await; @@ -462,7 +471,7 @@ impl Client { Err(BollardError::DockerResponseServerError { status_code: _, message: _, - }) => { + }) if !matches!(platform.as_ref().map(|s| s.as_str()), Some("linux/amd64")) => { self.pull_image_linux_amd64(descriptor).await?; } _ => { @@ -708,7 +717,7 @@ mod tests { let config = env::Config::load::().await?; let mut client = Client::new().await?; client.config = config; - client.pull_image("hello-world:latest").await?; + client.pull_image("hello-world:latest", None).await?; let image = client.bollard.inspect_image("hello-world:latest").await?; @@ -718,7 +727,7 @@ mod tests { let config = env::Config::load::().await?; let mut client = Client::new().await?; client.config = config; - client.pull_image("hello-world:latest").await?; + client.pull_image("hello-world:latest", None).await?; let image = client.bollard.inspect_image("hello-world:latest").await?; diff --git a/testcontainers/src/core/image/image_ext.rs b/testcontainers/src/core/image/image_ext.rs index b98cab6a..f5a76346 100644 --- a/testcontainers/src/core/image/image_ext.rs +++ b/testcontainers/src/core/image/image_ext.rs @@ -70,7 +70,7 @@ pub trait ImageExt { /// Sets the platform the container will be run on. /// - /// Platform in the format os[/arch[/variant]] used for image lookup. + /// Platform in the format `os[/arch[/variant]]` used for image lookup. /// /// # Examples /// diff --git a/testcontainers/src/runners/async_runner.rs b/testcontainers/src/runners/async_runner.rs index 54c2b2d5..e57cbbcf 100644 --- a/testcontainers/src/runners/async_runner.rs +++ b/testcontainers/src/runners/async_runner.rs @@ -315,7 +315,12 @@ where status_code: 404, .. }, )) => { - client.pull_image(&container_req.descriptor()).await?; + client + .pull_image( + &container_req.descriptor(), + container_req.platform().clone(), + ) + .await?; client.create_container(create_options, config).await } res => res, @@ -358,7 +363,12 @@ where async fn pull_image(self) -> Result> { let container_req = self.into(); let client = Client::lazy_client().await?; - client.pull_image(&container_req.descriptor()).await?; + client + .pull_image( + &container_req.descriptor(), + container_req.platform().clone(), + ) + .await?; Ok(container_req) } From ba9f79e33046e69df3042d3d2669a20e9146be8a Mon Sep 17 00:00:00 2001 From: Muhammad Mominul Huque Date: Mon, 22 Sep 2025 17:07:57 +0600 Subject: [PATCH 3/3] Use as_deref() --- testcontainers/src/core/client.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/testcontainers/src/core/client.rs b/testcontainers/src/core/client.rs index 311ca7a0..bd7a0a4a 100644 --- a/testcontainers/src/core/client.rs +++ b/testcontainers/src/core/client.rs @@ -454,8 +454,7 @@ impl Client { .from_image(descriptor) .platform( platform - .as_ref() - .map(|s| s.as_str()) + .as_deref() .unwrap_or_else(|| self.config.platform().unwrap_or_default()), ) .build(); @@ -471,7 +470,7 @@ impl Client { Err(BollardError::DockerResponseServerError { status_code: _, message: _, - }) if !matches!(platform.as_ref().map(|s| s.as_str()), Some("linux/amd64")) => { + }) if !matches!(platform.as_deref(), Some("linux/amd64")) => { self.pull_image_linux_amd64(descriptor).await?; } _ => {