From e34f144b274b8f1741648b55ddad6e8a8fdff6e0 Mon Sep 17 00:00:00 2001 From: queil <4584075+queil@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:50:05 +0000 Subject: [PATCH 01/22] fix: add chmod 1777 on shared volumes --- src/api/exec.rs | 23 +++++++++++++++++++++++ src/api/workspace/enter.rs | 10 +--------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/api/exec.rs b/src/api/exec.rs index 9747d3c..f1b7453 100644 --- a/src/api/exec.rs +++ b/src/api/exec.rs @@ -301,6 +301,29 @@ echo '[install] {}' Ok(()) } + pub async fn chmod(&self, container_id: &str, dir: &str) -> Result<(), AnyError> { + log::debug!("Changing permissions... ({})", &dir); + + let chmod_response = self + .output( + "chmod", + container_id, + Some(constants::ROOT_USER), + Some(vec![ + "sh", + "-c", + &format!( + "chmod -R 1777 {}", + &dir.replace("~", "${ROOZ_META_HOME}") + ), + ]), + ) + .await?; + + log::debug!("{}", chmod_response); + Ok(()) + } + pub async fn ensure_user(&self, container_id: &str) -> Result<(), AnyError> { let ensure_user_cmd = inject( format!( diff --git a/src/api/workspace/enter.rs b/src/api/workspace/enter.rs index 83464b7..bd1c208 100644 --- a/src/api/workspace/enter.rs +++ b/src/api/workspace/enter.rs @@ -107,18 +107,10 @@ impl<'a> WorkspaceApi<'a> { &config.sidecars[container_name].real_mounts }; - let chown_uid = if is_work_container { - &config.uid - } else { - &config.sidecars[container_name] - .uid - .unwrap_or_else(|| panic!("TODO: read default uid from the image")) - }; - for (target, _) in real_mounts { self.api .exec - .chown(&container_id, chown_uid, target.as_str()) + .chmod(&container_id, target.as_str()) .await?; } } From 364b93177142d745ab0f6a52ae5738fbf02f982e Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 14:53:01 +0200 Subject: [PATCH 02/22] fix: progress crash on docker backend --- src/api/image.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/api/image.rs b/src/api/image.rs index 769d8bc..57f0b13 100644 --- a/src/api/image.rs +++ b/src/api/image.rs @@ -164,17 +164,15 @@ impl<'a> ImageApi<'a> { while let Some(l) = image_info.next().await { match l { - Ok(CreateImageInfo { - id, - status: Some(status), - progress_detail: p, - .. - }) => { - if let (Some(id), status) = (&id, &status) { - progress.update(id, status, p.clone().unwrap().current, p.unwrap().total); + Ok(CreateImageInfo { id, status, progress_detail, .. }) => { + if let (Some(id), Some(status)) = (id, status) { + let (cur, tot) = match progress_detail { + Some(pd) => (pd.current, pd.total), + None => (None, None), + }; + progress.update(&id, &status, cur, tot); } } - Ok(msg) => panic!("{:?}", msg), Err(Error::DockerStreamError { error }) => eprintln!("{}", error), e => panic!("{:?}", e), }; From 2fc8211634a4baef0160d1b8506aba0f2c4f7a2d Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 15:03:03 +0200 Subject: [PATCH 03/22] fix: populate volume dies because /tmp is read-only on docker backend --- src/api/volume.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/api/volume.rs b/src/api/volume.rs index a4cc591..6d99d71 100644 --- a/src/api/volume.rs +++ b/src/api/volume.rs @@ -25,7 +25,7 @@ use bollard::{ service::Mount, }; use bollard_stubs::models::MountTypeEnum::VOLUME; -use bollard_stubs::models::VolumeCreateRequest; +use bollard_stubs::models::{MountTmpfsOptions, MountTypeEnum, VolumeCreateRequest}; impl<'a> VolumeApi<'a> { pub async fn get_all(&self, labels: &Labels) -> Result, AnyError> { @@ -504,7 +504,19 @@ impl<'a> VolumeApi<'a> { .one_shot( &format!("populate volume: {}", &spec.volume_name.as_str()), cmd, - Some(vec![mount]), + Some(vec![ + mount, + Mount { + typ: Some(MountTypeEnum::TMPFS), + target: Some("/tmp".to_string()), + tmpfs_options: Some(MountTmpfsOptions { + size_bytes: Some(16 * 1024 * 1024), + mode: Some(0o1777), + ..Default::default() + }), + ..Default::default() + }, + ]), None, None, ) From 39ab593f028a57f2428c96f029fd501eea98cadf Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 15:06:20 +0200 Subject: [PATCH 04/22] fix: drop config --- src/api/volume.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/api/volume.rs b/src/api/volume.rs index 6d99d71..aaae481 100644 --- a/src/api/volume.rs +++ b/src/api/volume.rs @@ -509,11 +509,6 @@ impl<'a> VolumeApi<'a> { Mount { typ: Some(MountTypeEnum::TMPFS), target: Some("/tmp".to_string()), - tmpfs_options: Some(MountTmpfsOptions { - size_bytes: Some(16 * 1024 * 1024), - mode: Some(0o1777), - ..Default::default() - }), ..Default::default() }, ]), From 70cff761dcf5e2f514a0f2cf4b2dff2edf98dec8 Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 15:10:08 +0200 Subject: [PATCH 05/22] fix: duplicate in v1 --- src/api/volume.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/api/volume.rs b/src/api/volume.rs index aaae481..c3ea4c8 100644 --- a/src/api/volume.rs +++ b/src/api/volume.rs @@ -509,6 +509,11 @@ impl<'a> VolumeApi<'a> { Mount { typ: Some(MountTypeEnum::TMPFS), target: Some("/tmp".to_string()), + tmpfs_options: Some(MountTmpfsOptions { + size_bytes: Some(16 * 1024 * 1024), + mode: Some(0o1777), + ..Default::default() + }), ..Default::default() }, ]), @@ -557,7 +562,19 @@ impl<'a> VolumeApi<'a> { .one_shot( &format!("populate volume: {}", &volume_name), cmd, - Some(vec![mount]), + Some(vec![ + mount, + Mount { + typ: Some(MountTypeEnum::TMPFS), + target: Some("/tmp".to_string()), + tmpfs_options: Some(MountTmpfsOptions { + size_bytes: Some(16 * 1024 * 1024), + mode: Some(0o1777), + ..Default::default() + }), + ..Default::default() + }, + ]), None, None, ) From b4e219ad39124ad565271e852eaa77fd23ee851d Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 15:19:28 +0200 Subject: [PATCH 06/22] fix: remove need for /tmp --- src/api/container.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/api/container.rs b/src/api/container.rs index c7350ae..30e3df1 100644 --- a/src/api/container.rs +++ b/src/api/container.rs @@ -46,6 +46,13 @@ pub fn inject(script: &str, name: &str) -> Vec { ), ] } +pub fn inject_sh(script: &str) -> Vec { + vec![ + "sh".to_string(), + "-c".to_string(), + script.trim().to_string(), + ] +} impl<'a> ContainerApi<'a> { pub async fn get_all(&self, labels: &Labels) -> Result, AnyError> { @@ -584,7 +591,7 @@ echo start > /tmp/exec_start image: Option<&str>, ) -> Result { let id = self.make_one_shot(name, mounts, uid, image).await?; - let cmd = Self::format_cmd(command); + let cmd = inject_sh(&command); let cmd = cmd.iter().map(|x| x.as_str()).collect::>(); let id_clone = id.clone(); From 5ba468d690055cf4744f276c031c69cafeda25ba Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 15:21:41 +0200 Subject: [PATCH 07/22] fix: remove need for /tmp --- src/api/container.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/container.rs b/src/api/container.rs index 30e3df1..99546de 100644 --- a/src/api/container.rs +++ b/src/api/container.rs @@ -508,7 +508,7 @@ EXEC_EXIT_CODE=$(cat /tmp/exec_exit) echo "Exec session ended: $EXEC_EXIT_CODE" exit $EXEC_EXIT_CODE"#; - let epv = inject(&wait_for_exec, "entrypoint.sh"); + let epv = inject_sh(&wait_for_exec); let entrypoint = epv.iter().map(String::as_str).collect(); let work_dir = "/tmp/one-shot"; let id = self From a40cad6a817c1bd260113af6ff69b8f616fb43fe Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 15:24:30 +0200 Subject: [PATCH 08/22] fix: improve display --- src/api/container.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/container.rs b/src/api/container.rs index 99546de..255c83a 100644 --- a/src/api/container.rs +++ b/src/api/container.rs @@ -394,7 +394,7 @@ impl<'a> ContainerApi<'a> { pub async fn create(&self, spec: RunSpec<'a>) -> Result { log::debug!( - "[{}: {:?}]: CREATE CONTAINER - name: {}, uid: {}, user: {}, image: {}, entrypoint: {}", + "[{}: {:?}]: CREATE CONTAINER - name: {}, uid: {}, user: {}, image: {}, entrypoint: {:?}", &spec.reason, spec.run_mode, spec.container_name, From 666a9cc2b59c39810f4d3480587936cd2d7e40f6 Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 15:27:19 +0200 Subject: [PATCH 09/22] fix: make_one_shot already mounts /tmp --- src/api/container.rs | 2 +- src/api/volume.rs | 28 ++-------------------------- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/src/api/container.rs b/src/api/container.rs index 255c83a..0ce5331 100644 --- a/src/api/container.rs +++ b/src/api/container.rs @@ -510,7 +510,7 @@ exit $EXEC_EXIT_CODE"#; let epv = inject_sh(&wait_for_exec); let entrypoint = epv.iter().map(String::as_str).collect(); - let work_dir = "/tmp/one-shot"; + let work_dir = "/tmp"; let id = self .create(RunSpec { reason: name, diff --git a/src/api/volume.rs b/src/api/volume.rs index c3ea4c8..11e1290 100644 --- a/src/api/volume.rs +++ b/src/api/volume.rs @@ -504,19 +504,7 @@ impl<'a> VolumeApi<'a> { .one_shot( &format!("populate volume: {}", &spec.volume_name.as_str()), cmd, - Some(vec![ - mount, - Mount { - typ: Some(MountTypeEnum::TMPFS), - target: Some("/tmp".to_string()), - tmpfs_options: Some(MountTmpfsOptions { - size_bytes: Some(16 * 1024 * 1024), - mode: Some(0o1777), - ..Default::default() - }), - ..Default::default() - }, - ]), + Some(vec![mount]), None, None, ) @@ -562,19 +550,7 @@ impl<'a> VolumeApi<'a> { .one_shot( &format!("populate volume: {}", &volume_name), cmd, - Some(vec![ - mount, - Mount { - typ: Some(MountTypeEnum::TMPFS), - target: Some("/tmp".to_string()), - tmpfs_options: Some(MountTmpfsOptions { - size_bytes: Some(16 * 1024 * 1024), - mode: Some(0o1777), - ..Default::default() - }), - ..Default::default() - }, - ]), + Some(vec![mount]), None, None, ) From de034625af3010c6cbbd9578912e5af01e5b6977 Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 15:32:34 +0200 Subject: [PATCH 10/22] fix: use inject_sh for all calls --- src/api/container.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/container.rs b/src/api/container.rs index 0ce5331..99664f4 100644 --- a/src/api/container.rs +++ b/src/api/container.rs @@ -579,7 +579,7 @@ echo start > /tmp/exec_start "#, command ); - inject(&cmd, "exec.sh") + inject_sh(&cmd) } pub async fn one_shot_output( @@ -591,7 +591,7 @@ echo start > /tmp/exec_start image: Option<&str>, ) -> Result { let id = self.make_one_shot(name, mounts, uid, image).await?; - let cmd = inject_sh(&command); + let cmd = Self::format_cmd(command); let cmd = cmd.iter().map(|x| x.as_str()).collect::>(); let id_clone = id.clone(); From d3ea9a36841907f35657788e12a843fe33a1f3c2 Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 15:45:25 +0200 Subject: [PATCH 11/22] fix: stop displaying content of RoozVolumeFile --- src/model/volume.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/model/volume.rs b/src/model/volume.rs index f537ca6..0bc886f 100644 --- a/src/model/volume.rs +++ b/src/model/volume.rs @@ -42,12 +42,20 @@ impl RoozVolumeRole { } } -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct RoozVolumeFile { pub file_path: String, pub data: String, } +impl std::fmt::Debug for RoozVolumeFile { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RoozVolumeFile") + .field("file_path", &self.file_path) + .field("data", &format!("<{} bytes>", self.data.len())) + .finish() + } +} #[derive(Debug, Clone)] pub struct RoozVolume { pub path: String, From 769a91dec3ec7183d7bccc8dd6b379daea3448c6 Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 15:56:05 +0200 Subject: [PATCH 12/22] fix: make exec trace --- src/api/exec.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/exec.rs b/src/api/exec.rs index f1b7453..8382a19 100644 --- a/src/api/exec.rs +++ b/src/api/exec.rs @@ -157,7 +157,7 @@ impl<'a> ExecApi<'a> { ) -> Result { #[cfg(not(windows))] { - log::debug!( + log::trace!( "[{}] exec: {:?} in working dir: {:?}", reason, cmd, From 2a921c1772dd7cf1aa38725e1b3ab2dd590b72cb Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 15:58:57 +0200 Subject: [PATCH 13/22] fix: debug lvl default --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index f2f4f6e..03d26ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,7 +39,7 @@ use util::labels::{self, Labels}; #[tokio::main] async fn main() -> Result<(), AnyError> { - env_logger::init(); + env_logger::Env::default().default_filter_or("rooz=debug"); log::debug!("Started"); From 57ae910b7af4fc9b518b24ad98fd138cdf1a175f Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 16:00:56 +0200 Subject: [PATCH 14/22] Revert "fix: debug lvl default" This reverts commit 2a921c1772dd7cf1aa38725e1b3ab2dd590b72cb. --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 03d26ad..f2f4f6e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,7 +39,7 @@ use util::labels::{self, Labels}; #[tokio::main] async fn main() -> Result<(), AnyError> { - env_logger::Env::default().default_filter_or("rooz=debug"); + env_logger::init(); log::debug!("Started"); From 477cdcb1cf45c59caf14b989cd28db8eb10a0750 Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 16:04:46 +0200 Subject: [PATCH 15/22] fix: change init dir to stop conflicting with /tmp mounted in one-shot --- src/cmd/init.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cmd/init.rs b/src/cmd/init.rs index e15bd71..7451b29 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -17,11 +17,11 @@ impl<'a> InitApi<'a> { async fn init_ssh(&self, image_id: &str, uid: &str) -> Result<(), AnyError> { let hostname = self.client.info().await?.name.unwrap_or("unknown".into()); let init_ssh = format!( - r#"mkdir -p /tmp/.ssh - KEYFILE=/tmp/.ssh/id_ed25519 + r#"mkdir -p /init/.ssh + KEYFILE=/init/.ssh/id_ed25519 ls "$KEYFILE.pub" > /dev/null 2>&1 || ssh-keygen -t ed25519 -N '' -f $KEYFILE -C rooz@{} cat "$KEYFILE.pub" - chmod 400 $KEYFILE && chown -R {} /tmp/.ssh + chmod 400 $KEYFILE && chown -R {} /init/.ssh "#, &hostname, &uid, ); @@ -30,7 +30,7 @@ impl<'a> InitApi<'a> { .one_shot( "init", init_ssh, - Some(vec![ssh::mount("/tmp/.ssh")]), + Some(vec![ssh::mount("/init/.ssh")]), None, Some(&image_id), ) @@ -48,13 +48,13 @@ impl<'a> InitApi<'a> { self.volume .ensure_mounts( &vec![RoozVolume::system_config_init( - "/tmp/sys", + "/init/sys", SystemConfig { age_key: Some(age_key.to_string().expose_secret().to_string()), gitconfig: Some( r#" [core] - sshCommand = ssh -i /tmp/.ssh/id_ed25519 -o UserKnownHostsFile=/tmp/.ssh/known_hosts + sshCommand = ssh -i /init/.ssh/id_ed25519 -o UserKnownHostsFile=/init/.ssh/known_hosts "# .trim() .to_string(), From 5346e67bf461cdfd913c621c90f22ef362d1959f Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 16:07:06 +0200 Subject: [PATCH 16/22] fix: change init dir to stop conflicting with /tmp mounted in one-shot --- src/api/system_config.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/system_config.rs b/src/api/system_config.rs index adcbe3f..5e19dc5 100644 --- a/src/api/system_config.rs +++ b/src/api/system_config.rs @@ -12,10 +12,10 @@ impl<'a> Api<'a> { .container .one_shot_output( "read-sys-config", - "ls /tmp/sys/rooz.config > /dev/null 2>&1 && cat /tmp/sys/rooz.config || echo ''" + "ls /init/sys/rooz.config > /dev/null 2>&1 && cat /init/sys/rooz.config || echo ''" .into(), Some(vec![ - RoozVolume::system_config_read("/tmp/sys").to_mount(None), + RoozVolume::system_config_read("/init/sys").to_mount(None), ]), None, None, From 3225fcb94996401078006f2215b116d8992845c0 Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 16:15:09 +0200 Subject: [PATCH 17/22] fix: always mount /tmp in one-shot --- src/api/container.rs | 11 ++++++----- src/main.rs | 4 +--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/api/container.rs b/src/api/container.rs index 99664f4..057df0e 100644 --- a/src/api/container.rs +++ b/src/api/container.rs @@ -517,14 +517,15 @@ exit $EXEC_EXIT_CODE"#; image: image.unwrap_or(constants::DEFAULT_IMAGE), container_name: &id::random_suffix("one-shot"), command: Some(entrypoint), - mounts: mounts.map(|mut m| { - m.extend_from_slice(&[Mount { + mounts: { + let mut m = mounts.unwrap_or_default(); + m.push(Mount { target: Some(work_dir.into()), typ: Some(MountTypeEnum::TMPFS), ..Default::default() - }]); - m - }), + }); + Some(m) + }, uid: uid.unwrap_or(constants::ROOT_UID), work_dir: Some(work_dir), ..Default::default() diff --git a/src/main.rs b/src/main.rs index f2f4f6e..bcdb31d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -198,9 +198,7 @@ async fn main() -> Result<(), AnyError> { } => workspace.remove(&name, false, force).await?, Cli { - command: Remove(RemoveParams { - name: _, force, .. - }), + command: Remove(RemoveParams { name: _, force, .. }), .. } => workspace.remove_all(force).await?, From 81fcc311cfc9d4829645b56e5f424ef836cc14b3 Mon Sep 17 00:00:00 2001 From: queil <4584075+queil@users.noreply.github.com> Date: Thu, 23 Apr 2026 16:05:12 +0000 Subject: [PATCH 18/22] rewrite: replace symlinks by subpaths --- Cargo.lock | 53 +---------------------------- Cargo.toml | 1 - examples/volume-v2.yaml | 2 ++ src/api/container.rs | 49 +-------------------------- src/api/sidecar.rs | 26 ++------------ src/api/volume.rs | 67 +++++++++++++++++++++++++------------ src/api/workspace/create.rs | 7 ---- src/api/workspace/enter.rs | 9 ++--- src/cmd/new.rs | 2 +- src/util/backend.rs | 48 +++++++++++++++++++++++--- 10 files changed, 103 insertions(+), 161 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 54464b5..d8029db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -790,17 +790,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "filetime" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" -dependencies = [ - "cfg-if", - "libc", - "libredox", -] - [[package]] name = "find-crate" version = "0.6.3" @@ -1823,10 +1812,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags", "libc", - "plain", - "redox_syscall 0.7.4", ] [[package]] @@ -2073,7 +2059,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link", ] @@ -2163,12 +2149,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - [[package]] name = "poly1305" version = "0.8.0" @@ -2340,15 +2320,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "redox_syscall" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" -dependencies = [ - "bitflags", -] - [[package]] name = "redox_users" version = "0.5.2" @@ -2438,7 +2409,6 @@ dependencies = [ "serde_yaml", "shellexpand", "tabled", - "tar", "tokio", ] @@ -2936,17 +2906,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tar" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" -dependencies = [ - "filetime", - "libc", - "xattr", -] - [[package]] name = "tempfile" version = "3.27.0" @@ -3715,16 +3674,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix 1.1.4", -] - [[package]] name = "yoke" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 0b6a46a..8bfe4c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,5 @@ serde_yaml = "0.9.34" shellexpand = "3.1.0" tabled = "0.20.0" tokio = { version = "1.46", features = ["rt-multi-thread", "macros", "io-std"] } -tar = "0.4.44" indexmap = { version ="2.13.0", features = ["serde"] } serde_with = "3.18.0" diff --git a/examples/volume-v2.yaml b/examples/volume-v2.yaml index 5a6678b..7b9959c 100644 --- a/examples/volume-v2.yaml +++ b/examples/volume-v2.yaml @@ -21,6 +21,8 @@ data: #local-file: # path: ./local.yaml +image: docker.io/alpine + mounts: ~/certs/my.key: cert ~/placeholder: empty-dir diff --git a/src/api/container.rs b/src/api/container.rs index 057df0e..54e4b53 100644 --- a/src/api/container.rs +++ b/src/api/container.rs @@ -11,7 +11,6 @@ use crate::{ use base64::{Engine as _, engine::general_purpose}; use bollard::{ - body_full, errors::Error::{self, DockerResponseServerError}, models::{ ContainerCreateBody, ContainerCreateResponse, ContainerInspectResponse, ContainerState, @@ -25,11 +24,9 @@ use bollard::{ }, }; -use crate::model::types::{TargetDir, VolumeFilesSpec}; use bollard_stubs::models::{MountTypeEnum, NetworkingConfig}; -use bollard_stubs::query_parameters::{UploadToContainerOptions, WaitContainerOptions}; +use bollard_stubs::query_parameters::WaitContainerOptions; use futures::{StreamExt, TryStreamExt, future}; -use std::time::{SystemTime, UNIX_EPOCH}; use std::{collections::HashMap, time::Duration}; use tokio::time::{sleep, timeout}; @@ -438,50 +435,6 @@ impl<'a> ContainerApi<'a> { Ok(container_id.clone()) } - pub async fn symlink_files( - &self, - container_id: &str, - mounts: &HashMap, - uid: Option, - ) -> Result<(), AnyError> { - let mut archive = tar::Builder::new(Vec::new()); - for (_, spec) in mounts { - for file in &spec.files { - log::debug!( - "Creating symlink: {} -> {}", - &file.user_file.as_str(), - &file.target_file.as_str() - ); - let mut header = tar::Header::new_gnu(); - header.set_size(0); - header.set_mode(0o777); - header.set_uid(uid.unwrap_or(constants::ROOT_UID_INT) as u64); - header.set_gid(uid.unwrap_or(constants::ROOT_UID_INT) as u64); - header.set_entry_type(tar::EntryType::Symlink); - header.set_mtime(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()); - archive.append_link( - &mut header, - file.user_file.as_str().trim_start_matches("/"), - file.target_file.as_str(), - )?; - } - } - let tar_bytes = archive.into_inner()?; - - self.client - .upload_to_container( - &container_id, - Some(UploadToContainerOptions { - path: "/".to_string(), - ..Default::default() - }), - body_full(tar_bytes.into()), - ) - .await?; - - Ok(()) - } - pub async fn start(&self, container_id: &str) -> Result<(), Error> { self.client .start_container(&container_id, None::) diff --git a/src/api/sidecar.rs b/src/api/sidecar.rs index 2763057..d34b5f9 100644 --- a/src/api/sidecar.rs +++ b/src/api/sidecar.rs @@ -137,12 +137,6 @@ impl<'a> WorkspaceApi<'a> { }) .await? { - // IMPORTANT: make symlinks to the mounted volumes before install so the - // mounted files/dirs are available - self.api - .container - .symlink_files(&container_id, &real_mounts, uid) - .await?; self.api.container.start(&container_id).await?; self.api .exec @@ -180,29 +174,15 @@ impl<'a> WorkspaceApi<'a> { } let latest_runtime_image = format!("{}:latest", runtime_image); - if let ContainerResult::Created { id: container_id } = self - .api + self.api .container .create(RunSpec { image: &latest_runtime_image, ..run_spec.clone() }) - .await? - { - self.api - .container - .symlink_files(&container_id, &real_mounts, uid) - .await?; - } + .await?; } else { - if let ContainerResult::Created { id: container_id } = - self.api.container.create(run_spec).await? - { - self.api - .container - .symlink_files(&container_id, &real_mounts, uid) - .await?; - } + self.api.container.create(run_spec).await?; } } diff --git a/src/api/volume.rs b/src/api/volume.rs index 11e1290..53a9a4c 100644 --- a/src/api/volume.rs +++ b/src/api/volume.rs @@ -25,7 +25,7 @@ use bollard::{ service::Mount, }; use bollard_stubs::models::MountTypeEnum::VOLUME; -use bollard_stubs::models::{MountTmpfsOptions, MountTypeEnum, VolumeCreateRequest}; +use bollard_stubs::models::{MountVolumeOptions, VolumeCreateRequest}; impl<'a> VolumeApi<'a> { pub async fn get_all(&self, labels: &Labels) -> Result, AnyError> { @@ -249,6 +249,9 @@ impl<'a> VolumeApi<'a> { mounts: HashMap, home_dir: Option<&str>, ) -> HashMap { + // Volume files are populated into a shadow dir so the one-shot container has a + // stable mount point. The subpath stored in FileSpec.target_file is the filename + // at the root of that volume, used as the subpath for the runtime mount. const SHADOW_ROOT_DIR: &str = "/var/lib/rooz"; mounts .iter() @@ -260,19 +263,15 @@ impl<'a> VolumeApi<'a> { executable, .. } => { - let shadow_subpath = &source_entry.clone().data.name(); - let shadow_file = Path::new(SHADOW_ROOT_DIR).join(shadow_subpath).join( - Path::new(&expanded_target) - .with_file_name(shadow_subpath) - .with_extension("data") - .to_string_lossy() - .trim_start_matches('/'), - ); + let data_name = source_entry.clone().data.name(); + let shadow_dir = format!("{}/{}", SHADOW_ROOT_DIR, data_name); + // Canonical filename at the volume root — used as the subpath for mounting. + let volume_subpath = format!("{}.data", data_name); ( - format!("{}/{}", SHADOW_ROOT_DIR.to_string(), shadow_subpath), + shadow_dir, Some(FileSpec { - target_file: TargetFile(shadow_file.to_string_lossy().into_owned()), + target_file: TargetFile(volume_subpath), user_file: UserFile(expanded_target), generator, executable, @@ -345,13 +344,35 @@ impl<'a> VolumeApi<'a> { &self, real_mounts: &HashMap, ) -> Result, AnyError> { - let mut mount_entries = HashMap::new(); - mount_entries.extend(real_mounts.clone()); - - let mounts_v2 = mount_entries - .into_iter() - .map(|(target, source)| Self::mount(&target, &source)) - .map(|v| (v.target.clone().unwrap().to_string(), v.clone())) + let mounts_v2 = real_mounts + .iter() + .flat_map(|(target, source)| { + if source.files.is_empty() { + // Directory: plain volume mount at the target path. + vec![Self::mount(target, source)] + } else { + // Files: one subpath volume mount per file, directly at the user path. + // The volume is populated with each file at a canonical path (e.g. git-config.data) + // and the subpath mount makes it appear at the intended location without any + // rootfs writes or symlinks. + source + .files + .iter() + .map(|f| Mount { + target: Some(f.user_file.as_str().to_string()), + source: Some(source.volume_name.as_str().to_string()), + typ: Some(VOLUME), + read_only: Some(false), + volume_options: Some(MountVolumeOptions { + subpath: Some(f.target_file.as_str().to_string()), + ..Default::default() + }), + ..Mount::default() + }) + .collect::>() + } + }) + .map(|v| (v.target.clone().unwrap(), v)) .collect::>() //dedupe entries to avoid duplicate mounts .into_values() .collect::>(); @@ -438,11 +459,15 @@ impl<'a> VolumeApi<'a> { ) -> Result<(), AnyError> { let mut cmds = Vec::new(); for f in &spec.files { - let parent_dir = Path::new(f.target_file.as_str()) + // target_file is the subpath inside the volume (e.g. "git-config.data"); + // combine with root_dir to get the absolute path inside the population container. + let full_path = Path::new(root_dir).join(f.target_file.as_str()); + let parent_dir = full_path .parent() .unwrap() .to_string_lossy() .into_owned(); + let full_path_str = full_path.to_string_lossy().into_owned(); let content = match &f.generator { ContentGenerator::Inline(content) => content.to_string(), @@ -477,9 +502,9 @@ impl<'a> VolumeApi<'a> { // IMPORTANT: never trim content so YAML multi-line strings are respected and can // control whitespace and most importantly EOLs general_purpose::STANDARD.encode(content), - f.target_file.as_str(), + full_path_str, if f.executable { - format!(" && chmod +x {}", f.target_file.as_str()) + format!(" && chmod +x {}", full_path_str) } else { "".to_string() } diff --git a/src/api/workspace/create.rs b/src/api/workspace/create.rs index 8913468..f8c3466 100644 --- a/src/api/workspace/create.rs +++ b/src/api/workspace/create.rs @@ -1,4 +1,3 @@ -use crate::model::types::{TargetDir, VolumeFilesSpec}; use crate::{ api::WorkspaceApi, model::{ @@ -7,14 +6,12 @@ use crate::{ }, util::ssh, }; -use std::collections::HashMap; use std::path::Path; impl<'a> WorkspaceApi<'a> { pub async fn create( &self, spec: &WorkSpec<'a>, - real_mounts: &HashMap, ) -> Result { let mut volumes = vec![]; @@ -76,10 +73,6 @@ impl<'a> WorkspaceApi<'a> { match self.api.container.create(run_spec).await? { ContainerResult::Created { id: container_id } => { - self.api - .container - .symlink_files(&container_id, &real_mounts, Some(spec.uid.parse::()?)) - .await?; if let Some(install) = spec.install.clone() { self.api.container.start(&container_id).await?; self.api diff --git a/src/api/workspace/enter.rs b/src/api/workspace/enter.rs index bd1c208..211f33e 100644 --- a/src/api/workspace/enter.rs +++ b/src/api/workspace/enter.rs @@ -96,10 +96,11 @@ impl<'a> WorkspaceApi<'a> { }; if !root { - if is_work_container { - // only for work containers: sidecars have a readonly rootfs so it would fail - self.api.exec.ensure_user(container_id).await?; - } + //TODO: run for tmp mode only + //if is_work_container { + // // only for work containers: sidecars have a readonly rootfs so it would fail + // self.api.exec.ensure_user(container_id).await?; + //} let real_mounts = if is_work_container { &config.real_mounts diff --git a/src/cmd/new.rs b/src/cmd/new.rs index 7c36f54..69bfc62 100644 --- a/src/cmd/new.rs +++ b/src/cmd/new.rs @@ -183,7 +183,7 @@ impl<'a> WorkspaceApi<'a> { ..*work_spec }; - let ws = self.create(&work_spec, &real_mounts).await?; + let ws = self.create(&work_spec).await?; if !cfg2.extra_repos.is_empty() { self.git .clone_extra_repos(clone_spec.clone(), cfg2.extra_repos) diff --git a/src/util/backend.rs b/src/util/backend.rs index 69347a1..4d3bec3 100644 --- a/src/util/backend.rs +++ b/src/util/backend.rs @@ -16,6 +16,13 @@ pub struct ContainerBackend { pub platform: String, } +fn parse_version(v: &str) -> Option<(u64, u64)> { + let mut parts = v.splitn(3, '.'); + let major = parts.next()?.parse().ok()?; + let minor = parts.next().unwrap_or("0").parse().ok()?; + Some((major, minor)) +} + impl ContainerBackend { pub async fn resolve(version: &SystemVersion, info: &SystemInfo) -> Result { fn backend(info: &SystemInfo, version: &SystemVersion) -> ContainerBackend { @@ -66,11 +73,44 @@ impl ContainerBackend { } } - let info = backend(&info, &version); - if let ContainerEngine::Unknown = info.engine { + let backend = backend(&info, &version); + if let ContainerEngine::Unknown = backend.engine { log::debug!("{:?}", &version); - log::debug!("{:?}", &info); + log::debug!("{:?}", &backend); + } + + // Subpath mounts require Docker >= 25.0 or Podman >= 4.7. + let version_str = match backend.engine { + ContainerEngine::Podman => version + .components + .as_ref() + .and_then(|cs| cs.iter().find(|c| c.name == "Podman Engine")) + .map(|c| c.version.as_str()) + .or_else(|| version.version.as_deref()) + .unwrap_or("0.0.0"), + _ => version.version.as_deref().unwrap_or("0.0.0"), + }; + + let (req_major, req_minor) = match backend.engine { + ContainerEngine::Podman => (4, 7), + _ => (25, 0), + }; + + if let Some((major, minor)) = parse_version(version_str) { + if major < req_major || (major == req_major && minor < req_minor) { + let engine_name = match backend.engine { + ContainerEngine::Podman => "Podman", + _ => "Docker", + }; + return Err(format!( + "rooz requires Docker >= 25.0 or Podman >= 4.7 for single-file mounts \ + (detected {} {})", + engine_name, version_str + ) + .into()); + } } - Ok(info) + + Ok(backend) } } From 346fa22f5c7b01e1d02141746bd3af14f8572c85 Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 18:12:06 +0200 Subject: [PATCH 19/22] fix: back /tmp by tmpfs during clone --- src/util/git.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/util/git.rs b/src/util/git.rs index ad69073..2441814 100644 --- a/src/util/git.rs +++ b/src/util/git.rs @@ -1,6 +1,6 @@ use gix_config::File; use std::collections::HashMap; - +use bollard_stubs::models::{Mount, MountTypeEnum}; use crate::{ api::{ExecApi, GitApi, container}, config::config::FileFormat, @@ -205,6 +205,12 @@ impl<'a> GitApi<'a> { mounts.push(vol.to_mount(None)); } + mounts.push(Mount { + target: Some("/tmp".to_string()), + typ: Some(MountTypeEnum::TMPFS), + ..Default::default() + }); + let run_spec = RunSpec { reason: "git-clone", image: &spec.image, From 95b5d4656bc3a2a7b8c06920435bcba1cd96ea8a Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 18:16:31 +0200 Subject: [PATCH 20/22] fix: use inject_sh --- src/util/git.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/git.rs b/src/util/git.rs index 2441814..8fe31ed 100644 --- a/src/util/git.rs +++ b/src/util/git.rs @@ -175,7 +175,7 @@ impl<'a> GitApi<'a> { } } - let clone_cmd = container::inject(&clone_script, "clone.sh"); + let clone_cmd = container::inject_sh(&clone_script); let labels = Labels::from(&[Labels::workspace(&spec.workspace_key), Labels::role("git")]); let mut mounts = vec![ssh::mount("/tmp/.ssh")]; @@ -210,7 +210,7 @@ impl<'a> GitApi<'a> { typ: Some(MountTypeEnum::TMPFS), ..Default::default() }); - + let run_spec = RunSpec { reason: "git-clone", image: &spec.image, From 057547f1e37f49156058f0730b9b567a636e53dd Mon Sep 17 00:00:00 2001 From: queil Date: Thu, 23 Apr 2026 18:20:04 +0200 Subject: [PATCH 21/22] fix: use inject_sh in ensure_user + execs --- src/api/exec.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/api/exec.rs b/src/api/exec.rs index 8382a19..06a6a69 100644 --- a/src/api/exec.rs +++ b/src/api/exec.rs @@ -7,7 +7,7 @@ use bollard::{ use bollard_stubs::models::ExecInspectResponse; use futures::{Stream, StreamExt}; -use crate::api::container::inject; +use crate::api::container::inject_sh; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use std::{io::Read, time::Duration}; use tokio::{ @@ -255,7 +255,7 @@ echo '[install] {}' {}"#, container_name, script ); - let install_cmd = inject(cmd.as_str(), "install.sh"); + let install_cmd = inject_sh(cmd.as_str()); let v = install_cmd.iter().map(|x| x.as_str()).collect::>(); self.tty( "install", @@ -312,10 +312,7 @@ echo '[install] {}' Some(vec![ "sh", "-c", - &format!( - "chmod -R 1777 {}", - &dir.replace("~", "${ROOZ_META_HOME}") - ), + &format!("chmod -R 1777 {}", &dir.replace("~", "${ROOZ_META_HOME}")), ]), ) .await?; @@ -325,15 +322,13 @@ echo '[install] {}' } pub async fn ensure_user(&self, container_id: &str) -> Result<(), AnyError> { - let ensure_user_cmd = inject( + let ensure_user_cmd = inject_sh( format!( r#"grep -q "^$ROOZ_META_USER:x:$ROOZ_META_UID" /etc/passwd && exit 0 sed -i "/:x:${{ROOZ_META_UID}}/d" /etc/passwd && \ echo "$ROOZ_META_USER:x:$ROOZ_META_UID:$ROOZ_META_UID:$ROOZ_META_USER:$ROOZ_META_HOME:/bin/sh" >> /etc/passwd"#, ) - .as_ref(), - "make_user.sh", - ); + .as_ref()); let ensure_user_output = self .output( From 9d167b03b8833b047958fe2e0d1886dcfab772c9 Mon Sep 17 00:00:00 2001 From: queil <4584075+queil@users.noreply.github.com> Date: Thu, 23 Apr 2026 19:28:15 +0000 Subject: [PATCH 22/22] Revert "rewrite: replace symlinks by subpaths" This reverts commit 81fcc311cfc9d4829645b56e5f424ef836cc14b3. --- Cargo.lock | 53 ++++++++++++++++++++++++++++- Cargo.toml | 1 + examples/volume-v2.yaml | 2 -- src/api/container.rs | 49 ++++++++++++++++++++++++++- src/api/sidecar.rs | 26 ++++++++++++-- src/api/volume.rs | 67 ++++++++++++------------------------- src/api/workspace/create.rs | 7 ++++ src/api/workspace/enter.rs | 9 +++-- src/cmd/new.rs | 2 +- src/util/backend.rs | 48 +++----------------------- 10 files changed, 161 insertions(+), 103 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8029db..54464b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -790,6 +790,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-crate" version = "0.6.3" @@ -1812,7 +1823,10 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ + "bitflags", "libc", + "plain", + "redox_syscall 0.7.4", ] [[package]] @@ -2059,7 +2073,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] @@ -2149,6 +2163,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "poly1305" version = "0.8.0" @@ -2320,6 +2340,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -2409,6 +2438,7 @@ dependencies = [ "serde_yaml", "shellexpand", "tabled", + "tar", "tokio", ] @@ -2906,6 +2936,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tar" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -3674,6 +3715,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.4", +] + [[package]] name = "yoke" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 8bfe4c4..0b6a46a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,5 +34,6 @@ serde_yaml = "0.9.34" shellexpand = "3.1.0" tabled = "0.20.0" tokio = { version = "1.46", features = ["rt-multi-thread", "macros", "io-std"] } +tar = "0.4.44" indexmap = { version ="2.13.0", features = ["serde"] } serde_with = "3.18.0" diff --git a/examples/volume-v2.yaml b/examples/volume-v2.yaml index 7b9959c..5a6678b 100644 --- a/examples/volume-v2.yaml +++ b/examples/volume-v2.yaml @@ -21,8 +21,6 @@ data: #local-file: # path: ./local.yaml -image: docker.io/alpine - mounts: ~/certs/my.key: cert ~/placeholder: empty-dir diff --git a/src/api/container.rs b/src/api/container.rs index 54e4b53..057df0e 100644 --- a/src/api/container.rs +++ b/src/api/container.rs @@ -11,6 +11,7 @@ use crate::{ use base64::{Engine as _, engine::general_purpose}; use bollard::{ + body_full, errors::Error::{self, DockerResponseServerError}, models::{ ContainerCreateBody, ContainerCreateResponse, ContainerInspectResponse, ContainerState, @@ -24,9 +25,11 @@ use bollard::{ }, }; +use crate::model::types::{TargetDir, VolumeFilesSpec}; use bollard_stubs::models::{MountTypeEnum, NetworkingConfig}; -use bollard_stubs::query_parameters::WaitContainerOptions; +use bollard_stubs::query_parameters::{UploadToContainerOptions, WaitContainerOptions}; use futures::{StreamExt, TryStreamExt, future}; +use std::time::{SystemTime, UNIX_EPOCH}; use std::{collections::HashMap, time::Duration}; use tokio::time::{sleep, timeout}; @@ -435,6 +438,50 @@ impl<'a> ContainerApi<'a> { Ok(container_id.clone()) } + pub async fn symlink_files( + &self, + container_id: &str, + mounts: &HashMap, + uid: Option, + ) -> Result<(), AnyError> { + let mut archive = tar::Builder::new(Vec::new()); + for (_, spec) in mounts { + for file in &spec.files { + log::debug!( + "Creating symlink: {} -> {}", + &file.user_file.as_str(), + &file.target_file.as_str() + ); + let mut header = tar::Header::new_gnu(); + header.set_size(0); + header.set_mode(0o777); + header.set_uid(uid.unwrap_or(constants::ROOT_UID_INT) as u64); + header.set_gid(uid.unwrap_or(constants::ROOT_UID_INT) as u64); + header.set_entry_type(tar::EntryType::Symlink); + header.set_mtime(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()); + archive.append_link( + &mut header, + file.user_file.as_str().trim_start_matches("/"), + file.target_file.as_str(), + )?; + } + } + let tar_bytes = archive.into_inner()?; + + self.client + .upload_to_container( + &container_id, + Some(UploadToContainerOptions { + path: "/".to_string(), + ..Default::default() + }), + body_full(tar_bytes.into()), + ) + .await?; + + Ok(()) + } + pub async fn start(&self, container_id: &str) -> Result<(), Error> { self.client .start_container(&container_id, None::) diff --git a/src/api/sidecar.rs b/src/api/sidecar.rs index d34b5f9..2763057 100644 --- a/src/api/sidecar.rs +++ b/src/api/sidecar.rs @@ -137,6 +137,12 @@ impl<'a> WorkspaceApi<'a> { }) .await? { + // IMPORTANT: make symlinks to the mounted volumes before install so the + // mounted files/dirs are available + self.api + .container + .symlink_files(&container_id, &real_mounts, uid) + .await?; self.api.container.start(&container_id).await?; self.api .exec @@ -174,15 +180,29 @@ impl<'a> WorkspaceApi<'a> { } let latest_runtime_image = format!("{}:latest", runtime_image); - self.api + if let ContainerResult::Created { id: container_id } = self + .api .container .create(RunSpec { image: &latest_runtime_image, ..run_spec.clone() }) - .await?; + .await? + { + self.api + .container + .symlink_files(&container_id, &real_mounts, uid) + .await?; + } } else { - self.api.container.create(run_spec).await?; + if let ContainerResult::Created { id: container_id } = + self.api.container.create(run_spec).await? + { + self.api + .container + .symlink_files(&container_id, &real_mounts, uid) + .await?; + } } } diff --git a/src/api/volume.rs b/src/api/volume.rs index 53a9a4c..11e1290 100644 --- a/src/api/volume.rs +++ b/src/api/volume.rs @@ -25,7 +25,7 @@ use bollard::{ service::Mount, }; use bollard_stubs::models::MountTypeEnum::VOLUME; -use bollard_stubs::models::{MountVolumeOptions, VolumeCreateRequest}; +use bollard_stubs::models::{MountTmpfsOptions, MountTypeEnum, VolumeCreateRequest}; impl<'a> VolumeApi<'a> { pub async fn get_all(&self, labels: &Labels) -> Result, AnyError> { @@ -249,9 +249,6 @@ impl<'a> VolumeApi<'a> { mounts: HashMap, home_dir: Option<&str>, ) -> HashMap { - // Volume files are populated into a shadow dir so the one-shot container has a - // stable mount point. The subpath stored in FileSpec.target_file is the filename - // at the root of that volume, used as the subpath for the runtime mount. const SHADOW_ROOT_DIR: &str = "/var/lib/rooz"; mounts .iter() @@ -263,15 +260,19 @@ impl<'a> VolumeApi<'a> { executable, .. } => { - let data_name = source_entry.clone().data.name(); - let shadow_dir = format!("{}/{}", SHADOW_ROOT_DIR, data_name); - // Canonical filename at the volume root — used as the subpath for mounting. - let volume_subpath = format!("{}.data", data_name); + let shadow_subpath = &source_entry.clone().data.name(); + let shadow_file = Path::new(SHADOW_ROOT_DIR).join(shadow_subpath).join( + Path::new(&expanded_target) + .with_file_name(shadow_subpath) + .with_extension("data") + .to_string_lossy() + .trim_start_matches('/'), + ); ( - shadow_dir, + format!("{}/{}", SHADOW_ROOT_DIR.to_string(), shadow_subpath), Some(FileSpec { - target_file: TargetFile(volume_subpath), + target_file: TargetFile(shadow_file.to_string_lossy().into_owned()), user_file: UserFile(expanded_target), generator, executable, @@ -344,35 +345,13 @@ impl<'a> VolumeApi<'a> { &self, real_mounts: &HashMap, ) -> Result, AnyError> { - let mounts_v2 = real_mounts - .iter() - .flat_map(|(target, source)| { - if source.files.is_empty() { - // Directory: plain volume mount at the target path. - vec![Self::mount(target, source)] - } else { - // Files: one subpath volume mount per file, directly at the user path. - // The volume is populated with each file at a canonical path (e.g. git-config.data) - // and the subpath mount makes it appear at the intended location without any - // rootfs writes or symlinks. - source - .files - .iter() - .map(|f| Mount { - target: Some(f.user_file.as_str().to_string()), - source: Some(source.volume_name.as_str().to_string()), - typ: Some(VOLUME), - read_only: Some(false), - volume_options: Some(MountVolumeOptions { - subpath: Some(f.target_file.as_str().to_string()), - ..Default::default() - }), - ..Mount::default() - }) - .collect::>() - } - }) - .map(|v| (v.target.clone().unwrap(), v)) + let mut mount_entries = HashMap::new(); + mount_entries.extend(real_mounts.clone()); + + let mounts_v2 = mount_entries + .into_iter() + .map(|(target, source)| Self::mount(&target, &source)) + .map(|v| (v.target.clone().unwrap().to_string(), v.clone())) .collect::>() //dedupe entries to avoid duplicate mounts .into_values() .collect::>(); @@ -459,15 +438,11 @@ impl<'a> VolumeApi<'a> { ) -> Result<(), AnyError> { let mut cmds = Vec::new(); for f in &spec.files { - // target_file is the subpath inside the volume (e.g. "git-config.data"); - // combine with root_dir to get the absolute path inside the population container. - let full_path = Path::new(root_dir).join(f.target_file.as_str()); - let parent_dir = full_path + let parent_dir = Path::new(f.target_file.as_str()) .parent() .unwrap() .to_string_lossy() .into_owned(); - let full_path_str = full_path.to_string_lossy().into_owned(); let content = match &f.generator { ContentGenerator::Inline(content) => content.to_string(), @@ -502,9 +477,9 @@ impl<'a> VolumeApi<'a> { // IMPORTANT: never trim content so YAML multi-line strings are respected and can // control whitespace and most importantly EOLs general_purpose::STANDARD.encode(content), - full_path_str, + f.target_file.as_str(), if f.executable { - format!(" && chmod +x {}", full_path_str) + format!(" && chmod +x {}", f.target_file.as_str()) } else { "".to_string() } diff --git a/src/api/workspace/create.rs b/src/api/workspace/create.rs index f8c3466..8913468 100644 --- a/src/api/workspace/create.rs +++ b/src/api/workspace/create.rs @@ -1,3 +1,4 @@ +use crate::model::types::{TargetDir, VolumeFilesSpec}; use crate::{ api::WorkspaceApi, model::{ @@ -6,12 +7,14 @@ use crate::{ }, util::ssh, }; +use std::collections::HashMap; use std::path::Path; impl<'a> WorkspaceApi<'a> { pub async fn create( &self, spec: &WorkSpec<'a>, + real_mounts: &HashMap, ) -> Result { let mut volumes = vec![]; @@ -73,6 +76,10 @@ impl<'a> WorkspaceApi<'a> { match self.api.container.create(run_spec).await? { ContainerResult::Created { id: container_id } => { + self.api + .container + .symlink_files(&container_id, &real_mounts, Some(spec.uid.parse::()?)) + .await?; if let Some(install) = spec.install.clone() { self.api.container.start(&container_id).await?; self.api diff --git a/src/api/workspace/enter.rs b/src/api/workspace/enter.rs index 211f33e..bd1c208 100644 --- a/src/api/workspace/enter.rs +++ b/src/api/workspace/enter.rs @@ -96,11 +96,10 @@ impl<'a> WorkspaceApi<'a> { }; if !root { - //TODO: run for tmp mode only - //if is_work_container { - // // only for work containers: sidecars have a readonly rootfs so it would fail - // self.api.exec.ensure_user(container_id).await?; - //} + if is_work_container { + // only for work containers: sidecars have a readonly rootfs so it would fail + self.api.exec.ensure_user(container_id).await?; + } let real_mounts = if is_work_container { &config.real_mounts diff --git a/src/cmd/new.rs b/src/cmd/new.rs index 69bfc62..7c36f54 100644 --- a/src/cmd/new.rs +++ b/src/cmd/new.rs @@ -183,7 +183,7 @@ impl<'a> WorkspaceApi<'a> { ..*work_spec }; - let ws = self.create(&work_spec).await?; + let ws = self.create(&work_spec, &real_mounts).await?; if !cfg2.extra_repos.is_empty() { self.git .clone_extra_repos(clone_spec.clone(), cfg2.extra_repos) diff --git a/src/util/backend.rs b/src/util/backend.rs index 4d3bec3..69347a1 100644 --- a/src/util/backend.rs +++ b/src/util/backend.rs @@ -16,13 +16,6 @@ pub struct ContainerBackend { pub platform: String, } -fn parse_version(v: &str) -> Option<(u64, u64)> { - let mut parts = v.splitn(3, '.'); - let major = parts.next()?.parse().ok()?; - let minor = parts.next().unwrap_or("0").parse().ok()?; - Some((major, minor)) -} - impl ContainerBackend { pub async fn resolve(version: &SystemVersion, info: &SystemInfo) -> Result { fn backend(info: &SystemInfo, version: &SystemVersion) -> ContainerBackend { @@ -73,44 +66,11 @@ impl ContainerBackend { } } - let backend = backend(&info, &version); - if let ContainerEngine::Unknown = backend.engine { + let info = backend(&info, &version); + if let ContainerEngine::Unknown = info.engine { log::debug!("{:?}", &version); - log::debug!("{:?}", &backend); - } - - // Subpath mounts require Docker >= 25.0 or Podman >= 4.7. - let version_str = match backend.engine { - ContainerEngine::Podman => version - .components - .as_ref() - .and_then(|cs| cs.iter().find(|c| c.name == "Podman Engine")) - .map(|c| c.version.as_str()) - .or_else(|| version.version.as_deref()) - .unwrap_or("0.0.0"), - _ => version.version.as_deref().unwrap_or("0.0.0"), - }; - - let (req_major, req_minor) = match backend.engine { - ContainerEngine::Podman => (4, 7), - _ => (25, 0), - }; - - if let Some((major, minor)) = parse_version(version_str) { - if major < req_major || (major == req_major && minor < req_minor) { - let engine_name = match backend.engine { - ContainerEngine::Podman => "Podman", - _ => "Docker", - }; - return Err(format!( - "rooz requires Docker >= 25.0 or Podman >= 4.7 for single-file mounts \ - (detected {} {})", - engine_name, version_str - ) - .into()); - } + log::debug!("{:?}", &info); } - - Ok(backend) + Ok(info) } }