Skip to content

Commit

Permalink
feat: support copy files to container
Browse files Browse the repository at this point in the history
  • Loading branch information
guenhter committed Sep 12, 2024
1 parent 10273d0 commit 4249029
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 9 deletions.
1 change: 1 addition & 0 deletions testcontainers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ signal-hook = { version = "0.3", optional = true }
thiserror = "1.0.60"
tokio = { version = "1", features = ["macros", "fs", "rt-multi-thread"] }
tokio-stream = "0.1.15"
tokio-tar = "0.3.1"
tokio-util = { version = "0.7.10", features = ["io"] }
url = { version = "2", features = ["serde"] }

Expand Down
1 change: 1 addition & 0 deletions testcontainers/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod image;
pub(crate) mod async_drop;
pub(crate) mod client;
pub(crate) mod containers;
pub(crate) mod copy;
pub(crate) mod env;
pub mod error;
pub mod logs;
Expand Down
41 changes: 39 additions & 2 deletions testcontainers/src/core/client.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
use std::{io, str::FromStr};
use std::{
io::{self},
str::FromStr,
};

use bollard::{
auth::DockerCredentials,
container::{Config, CreateContainerOptions, LogOutput, LogsOptions, RemoveContainerOptions},
container::{
Config, CreateContainerOptions, LogOutput, LogsOptions, RemoveContainerOptions,
UploadToContainerOptions,
},
errors::Error as BollardError,
exec::{CreateExecOptions, StartExecOptions, StartExecResults},
image::CreateImageOptions,
Expand All @@ -16,6 +22,7 @@ use url::Url;

use crate::core::{
client::exec::ExecResult,
copy::{CopyToContaienrError, CopyToContainer},
env,
env::ConfigurationError,
logs::{
Expand Down Expand Up @@ -81,6 +88,10 @@ pub enum ClientError {
InitExec(BollardError),
#[error("failed to inspect exec command: {0}")]
InspectExec(BollardError),
#[error("failed to upload data to container: {0}")]
UploadToContainerError(BollardError),
#[error("failed to prepare data for copy-to-container: {0}")]
CopyToContaienrError(CopyToContaienrError),
}

/// The internal client.
Expand Down Expand Up @@ -276,6 +287,32 @@ impl Client {
.map_err(ClientError::StartContainer)
}

pub(crate) async fn copy_to_container(
&self,
container_id: impl Into<String>,
copy_to_container: &CopyToContainer,
) -> Result<(), ClientError> {
let container_id: String = container_id.into();
let target_directory = copy_to_container
.target_directory()
.map_err(ClientError::CopyToContaienrError)?;

let options = UploadToContainerOptions {
path: target_directory,
no_overwrite_dir_non_dir: "false".into(),
};

let tar = copy_to_container
.tar()
.await
.map_err(ClientError::CopyToContaienrError)?;

self.bollard
.upload_to_container::<String>(&container_id, Some(options), tar)
.await
.map_err(ClientError::UploadToContainerError)
}

pub(crate) async fn pull_image(&self, descriptor: &str) -> Result<(), ClientError> {
let pull_options = Some(CreateImageOptions {
from_image: descriptor,
Expand Down
13 changes: 11 additions & 2 deletions testcontainers/src/core/containers/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use bollard_stubs::models::ResourcesUlimits;

use crate::{
core::{
logs::consumer::LogConsumer, mounts::Mount, ports::ContainerPort, ContainerState,
ExecCommand, WaitFor,
copy::CopyToContainer, logs::consumer::LogConsumer, mounts::Mount, ports::ContainerPort,
ContainerState, ExecCommand, WaitFor,
},
Image, TestcontainersError,
};
Expand All @@ -28,6 +28,7 @@ pub struct ContainerRequest<I: Image> {
pub(crate) env_vars: BTreeMap<String, String>,
pub(crate) hosts: BTreeMap<String, Host>,
pub(crate) mounts: Vec<Mount>,
pub(crate) copy_to_sources: Vec<CopyToContainer>,
pub(crate) ports: Option<Vec<PortMapping>>,
pub(crate) ulimits: Option<Vec<ResourcesUlimits>>,
pub(crate) privileged: bool,
Expand Down Expand Up @@ -95,6 +96,13 @@ impl<I: Image> ContainerRequest<I> {
self.image.mounts().into_iter().chain(self.mounts.iter())
}

pub fn copy_to_sources(&self) -> impl Iterator<Item = &CopyToContainer> {
self.image
.copy_to_sources()
.into_iter()
.chain(self.copy_to_sources.iter())
}

pub fn ports(&self) -> Option<&Vec<PortMapping>> {
self.ports.as_ref()
}
Expand Down Expand Up @@ -175,6 +183,7 @@ impl<I: Image> From<I> for ContainerRequest<I> {
env_vars: BTreeMap::default(),
hosts: BTreeMap::default(),
mounts: Vec::new(),
copy_to_sources: Vec::new(),
ports: None,
ulimits: None,
privileged: false,
Expand Down
97 changes: 97 additions & 0 deletions testcontainers/src/core/copy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use std::{
io,
path::{self, Path, PathBuf},
};

#[derive(Debug, Clone)]
pub struct CopyToContainer {
pub target: String,
pub source: CopyDataSource,
}

#[derive(Debug, Clone)]
pub enum CopyDataSource {
File(PathBuf),
Data(Vec<u8>),
}

#[derive(Debug, thiserror::Error)]
pub enum CopyToContaienrError {
#[error("io failed with error: {0}")]
IoError(io::Error),
#[error("failed to get the path name: {0}")]
PathNameError(String),
}

impl CopyToContainer {
pub fn target_directory(&self) -> Result<String, CopyToContaienrError> {
match path::Path::new(&self.target).parent() {
Some(v) => Ok(v.display().to_string()),
None => return Err(CopyToContaienrError::PathNameError(self.target.clone())),
}
}

pub async fn tar(&self) -> Result<bytes::Bytes, CopyToContaienrError> {
self.source.tar(&self.target).await
}
}

impl CopyDataSource {
pub async fn tar(
&self,
target_path: impl Into<String>,
) -> Result<bytes::Bytes, CopyToContaienrError> {
let target_path: String = target_path.into();
let mut ar = tokio_tar::Builder::new(Vec::new());

match self {
CopyDataSource::File(file_path) => {
let mut f = &mut tokio::fs::File::open(file_path)
.await
.map_err(CopyToContaienrError::IoError)?;
ar.append_file(&target_path, &mut f)
.await
.map_err(CopyToContaienrError::IoError)?;
}
CopyDataSource::Data(data) => {
let path = path::Path::new(&target_path);
let file_name = match path.file_name() {
Some(v) => v,
None => return Err(CopyToContaienrError::PathNameError(target_path)),
};

let mut header = tokio_tar::Header::new_gnu();
header.set_size(data.len() as u64);
header.set_mode(0o0644);
header.set_cksum();

ar.append_data(&mut header, file_name, data.as_slice())
.await
.map_err(CopyToContaienrError::IoError)?;
}
}

let bytes = ar
.into_inner()
.await
.map_err(CopyToContaienrError::IoError)?;

Ok(bytes::Bytes::copy_from_slice(bytes.as_slice()))
}
}

impl From<&Path> for CopyDataSource {
fn from(value: &Path) -> Self {
CopyDataSource::File(value.to_path_buf())
}
}
impl From<PathBuf> for CopyDataSource {
fn from(value: PathBuf) -> Self {
CopyDataSource::File(value)
}
}
impl From<Vec<u8>> for CopyDataSource {
fn from(value: Vec<u8>) -> Self {
CopyDataSource::Data(value)
}
}
13 changes: 11 additions & 2 deletions testcontainers/src/core/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ use std::{borrow::Cow, fmt::Debug};
pub use exec::ExecCommand;
pub use image_ext::ImageExt;

use super::ports::{ContainerPort, Ports};
use crate::{
core::{mounts::Mount, WaitFor},
core::{
copy::CopyToContainer,
mounts::Mount,
ports::{ContainerPort, Ports},
WaitFor,
},
TestcontainersError,
};

Expand Down Expand Up @@ -54,6 +58,11 @@ where
std::iter::empty()
}

/// Returns the files to be copied into the container at startup.
fn copy_to_sources(&self) -> impl IntoIterator<Item = &CopyToContainer> {
std::iter::empty()
}

/// Returns the [entrypoint](`https://docs.docker.com/reference/dockerfile/#entrypoint`) this image needs to be created with.
fn entrypoint(&self) -> Option<&str> {
None
Expand Down
23 changes: 22 additions & 1 deletion testcontainers/src/core/image/image_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ use std::time::Duration;
use bollard_stubs::models::ResourcesUlimits;

use crate::{
core::{logs::consumer::LogConsumer, CgroupnsMode, ContainerPort, Host, Mount, PortMapping},
core::{
copy::{CopyDataSource, CopyToContainer},
logs::consumer::LogConsumer,
CgroupnsMode, ContainerPort, Host, Mount, PortMapping,
},
ContainerRequest, Image,
};

Expand Down Expand Up @@ -54,6 +58,10 @@ pub trait ImageExt<I: Image> {
/// Adds a mount to the container.
fn with_mount(self, mount: impl Into<Mount>) -> ContainerRequest<I>;

/// Copies some source into the container as file
fn with_copy_to(self, target: impl Into<String>, source: CopyDataSource)
-> ContainerRequest<I>;

/// Adds a port mapping to the container, mapping the host port to the container's internal port.
///
/// # Examples
Expand Down Expand Up @@ -168,6 +176,19 @@ impl<RI: Into<ContainerRequest<I>>, I: Image> ImageExt<I> for RI {
container_req
}

fn with_copy_to(
self,
target: impl Into<String>,
source: CopyDataSource,
) -> ContainerRequest<I> {
let mut container_req = self.into();
let target: String = target.into();
container_req
.copy_to_sources
.push(CopyToContainer { target, source });
container_req
}

fn with_mapped_port(
self,
host_port: u16,
Expand Down
3 changes: 2 additions & 1 deletion testcontainers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ pub mod core;
#[cfg_attr(docsrs, doc(cfg(feature = "blocking")))]
pub use crate::core::Container;
pub use crate::core::{
error::TestcontainersError, ContainerAsync, ContainerRequest, Image, ImageExt,
copy::CopyDataSource, error::TestcontainersError, ContainerAsync, ContainerRequest, Image,
ImageExt,
};

#[cfg(feature = "watchdog")]
Expand Down
10 changes: 10 additions & 0 deletions testcontainers/src/runners/async_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use bollard_stubs::models::{HostConfigCgroupnsModeEnum, ResourcesUlimits};
use crate::{
core::{
client::{Client, ClientError},
copy::CopyToContainer,
error::{Result, WaitContainerError},
mounts::{AccessMode, Mount, MountType},
network::Network,
Expand Down Expand Up @@ -212,6 +213,15 @@ where
res => res,
}?;

let copy_to_sources: Vec<&CopyToContainer> =
container_req.copy_to_sources().map(Into::into).collect();

for copy_to_source in copy_to_sources {
client
.copy_to_container(&container_id, &copy_to_source)
.await?;
}

#[cfg(feature = "watchdog")]
if client.config.command() == crate::core::env::Command::Remove {
crate::watchdog::register(container_id.clone());
Expand Down
22 changes: 21 additions & 1 deletion testcontainers/tests/async_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use testcontainers::{
CmdWaitFor, ExecCommand, WaitFor,
},
runners::AsyncRunner,
GenericImage, *,
CopyDataSource, GenericImage, Image, ImageExt,
};
use tokio::io::AsyncReadExt;

Expand Down Expand Up @@ -199,3 +199,23 @@ async fn async_run_with_log_consumer() -> anyhow::Result<()> {
rx.recv()?; // notification from consumer
Ok(())
}

#[tokio::test]
async fn async_copy_files_to_container() -> anyhow::Result<()> {
let container = GenericImage::new("alpine", "latest")
.with_wait_for(WaitFor::seconds(2))
.with_copy_to(
"/tmp/somefile",
CopyDataSource::from("foobar".to_string().into_bytes()),
)
.with_cmd(vec!["cat", "/tmp/somefile"])
.start()
.await?;

let mut out = String::new();
container.stdout(false).read_to_string(&mut out).await?;

assert!(out.contains("foobar"));

Ok(())
}
21 changes: 21 additions & 0 deletions testcontainers/tests/sync_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,24 @@ fn sync_run_with_log_consumer() -> anyhow::Result<()> {
rx.recv()?; // notification from consumer
Ok(())
}

#[test]
fn sync_copy_files_to_container() -> anyhow::Result<()> {
let _ = pretty_env_logger::try_init();

let container = GenericImage::new("alpine", "latest")
.with_wait_for(WaitFor::seconds(2))
.with_copy_to(
"/tmp/somefile",
CopyDataSource::Data("foobar".to_string().into_bytes()),
)
.with_cmd(vec!["cat", "/tmp/somefile"])
.start()?;

let mut out = String::new();
container.stdout(false).read_to_string(&mut out)?;

assert!(out.contains("foobar"));

Ok(())
}

0 comments on commit 4249029

Please sign in to comment.