From c2775539b58b458b3af55d5675ddb5c1b64b5b53 Mon Sep 17 00:00:00 2001 From: Archie Atkinson Date: Sat, 19 Jul 2025 14:16:26 +0100 Subject: [PATCH] Adds ability for users to copy files into container --- README.md | 7 +++++++ flake.nix | 1 + src/configuration.rs | 12 ++++++++++++ src/docker.rs | 15 +++++++++++++++ tests/docker.rs | 32 ++++++++++++++++++++++++++++++++ tests/test_utils/base.rs | 5 +++++ 6 files changed, 72 insertions(+) diff --git a/README.md b/README.md index 6ecb249..b934bf4 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ Each environment is defined in a `environment` sub-table, with the name used to | `dockerfile` | String | The path to a dockerfile, this will be build and passed to `docker create`. This or the `image` field must be present. | `dockerfile = "$HOME/dockerfile"` | | `entry_cmd` | String| The command that will be run in the container when the environment is started. Passed to `docker exec`. This is a required field. | `entry_cmd = ["/bin/bash"]` | | `entry_options` | String Array | Options passed to `docker exec` for the `entry_cmd` | `entry_options = ["-it"]`| +| `cp_cmds`| String Array | A list of commands to copy files to or from the container. Use `CONTAINER`as a placeholder for the container name. Passed directly to `docker cp` | `cp_cmds = [" -L /home/my_script.sh CONTAINER:/home/init_script.sh"]`| | `exec_cmds`| String Array | A list of additional commands that will be run in the container when it is created, useful for adding additional packages. Passed to `docker exec` | `exec_cmds = ["apt update -y", "apt install -y cowsay"]`| | `exec_options` | String Array | Docker CLI options passed to the `docker exec` for all `exec_cmds` | `exec_options = ["-u", "user"]`| | `create_options` | String Array | Docker CLI options passed to `docker create` command. Note that `--name` is not allowed as that is provided by `berth`| `create_options = ["--privileged"]`| @@ -272,3 +273,9 @@ A difference from the container naming convention is that the third part is a `s - Add snapshot testing - Add docker mocking - Add check if docker is up in test ctor +- Fix cleanup not always working +- Fix docker containers not removing them selves if build fails +- Improves errors when build fails due to docker exec +- Add dependciy system for files outside of berth file for update tracking +- Add docker cp support +- Add build context around dockerfile inputs to allow COPY to work diff --git a/flake.nix b/flake.nix index 0d6754d..d240717 100644 --- a/flake.nix +++ b/flake.nix @@ -26,6 +26,7 @@ rustToolchain pkg-config openssl + bacon ]; }; } diff --git a/src/configuration.rs b/src/configuration.rs index 6525677..aefb3b4 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -108,6 +108,9 @@ pub struct TomlEnvironment { #[serde(default)] entry_options: Vec, + #[serde(default)] + cp_cmds: Vec, + #[serde(default)] exec_cmds: Vec, @@ -137,6 +140,9 @@ pub struct TomlPreset { #[serde(default)] entry_options: Vec, + #[serde(default)] + cp_cmds: Vec, + #[serde(default)] exec_cmds: Vec, @@ -170,6 +176,7 @@ pub struct Environment { pub exec_cmds: Vec, pub exec_options: Vec, pub create_options: Vec, + pub cp_cmds: Vec, } pub struct Configuration { @@ -359,16 +366,20 @@ impl Configuration { if !preset.entry_cmd.is_empty() { env.entry_cmd = preset.entry_cmd.clone(); } + if !preset.provided_image.is_empty() { env.provided_image = preset.provided_image.clone(); } + if !preset.dockerfile.is_empty() { env.dockerfile = preset.dockerfile.clone(); } + env.entry_options.extend_from_slice(&preset.entry_options); env.exec_cmds.extend_from_slice(&preset.exec_cmds); env.exec_options.extend_from_slice(&preset.exec_options); env.create_options.extend_from_slice(&preset.create_options); + env.cp_cmds.extend_from_slice(&preset.cp_cmds); } } @@ -474,6 +485,7 @@ impl Configuration { exec_cmds: env.exec_cmds, exec_options: env.exec_options, create_options: env.create_options, + cp_cmds: env.cp_cmds, }; let mut hasher = DefaultHasher::new(); diff --git a/src/docker.rs b/src/docker.rs index 8601fe6..39ed8bd 100644 --- a/src/docker.rs +++ b/src/docker.rs @@ -133,6 +133,7 @@ impl DockerHandler { self.create_container()?; self.start_container().await?; + self.copy_commands()?; self.exec_setup_commands()?; spinner.finish_and_clear(); @@ -268,6 +269,20 @@ impl DockerHandler { Ok(()) } + fn copy_commands(&self) -> Result<()> { + for cmd in &self.env.cp_cmds { + let mut args = vec!["cp"]; + + let fixed_string = cmd.clone().replace("CONTAINER", &self.env.name); + + let split_cmd = shell_words::split(&fixed_string).unwrap(); + args.extend(split_cmd.iter().map(|s| s.as_str())); + + Self::run_docker_command(args)?; + } + Ok(()) + } + pub async fn stop_container_if_running(&self) -> Result<()> { if self.is_container_running().await? { self.docker diff --git a/tests/docker.rs b/tests/docker.rs index b462c8f..80f5582 100644 --- a/tests/docker.rs +++ b/tests/docker.rs @@ -55,6 +55,38 @@ fn mount() -> Result<()> { Ok(()) } +#[test] +#[serial] +fn copy_cmds() -> Result<()> { + let mut tmp_file = NamedTempFile::new().unwrap(); + let tmp_file_path = tmp_file.path().to_str().unwrap().to_string(); + let file_text = "Hello World"; + writeln!(tmp_file, "{}", file_text).unwrap(); + + TestHarness::new() + .config(&formatdoc!( + r#" + image = "alpine:edge" + entry_cmd = "/bin/ash" + cp_cmds = ["{} CONTAINER:{}"] + create_options = ["-it"] + entry_options = ["-it"] + "#, + tmp_file_path, + tmp_file_path + ))? + .args(vec!["--config-path", "[config_path]", "[name]"])? + .run(DEFAULT_TIMEOUT)? + .send_line(&format!("cat {}", tmp_file_path))? + .expect_string(file_text)? + .send_line("exit")? + .expect_terminate()? + .success()?; + + tmp_file.close().unwrap(); + Ok(()) +} + #[test] #[serial] fn exec_cmds() -> Result<()> { diff --git a/tests/test_utils/base.rs b/tests/test_utils/base.rs index d94872e..197ee6e 100644 --- a/tests/test_utils/base.rs +++ b/tests/test_utils/base.rs @@ -111,6 +111,11 @@ impl TestBase { if let Some(dir) = &self.working_dir { command.current_dir(dir); } + + if let Ok(docker_host) = std::env::var("DOCKER_HOST") { + self.envs.push(("DOCKER_HOST".into(), docker_host)); + } + command.env_clear(); command.args(self.args.clone()); command.envs(self.envs.clone());