Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support copy files to container #730

Merged
merged 2 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted

We rely on `rustfmt` (`nightly`):
```shell
cargo +nightly fmt - - all
cargo +nightly fmt --all -- --check
```

### Commits
Expand Down
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(())
}
Loading