From 8fea469ac8a2f783806f658a5a1574f3054fbdce Mon Sep 17 00:00:00 2001 From: queil <4584075+queil@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:41:43 +0000 Subject: [PATCH] chore: migrate volumes to v2 --- src/api/config.rs | 80 ++++++-------- src/api/sidecar.rs | 15 +-- src/api/system_config.rs | 11 +- src/api/volume.rs | 213 +++++++++++------------------------- src/api/workspace/create.rs | 2 +- src/cmd/init.rs | 44 ++++---- src/cmd/new.rs | 12 +- src/constants.rs | 1 + src/main.rs | 14 ++- src/model/types.rs | 8 +- src/model/volume.rs | 137 ++++++++--------------- src/util/git.rs | 29 +++-- tests/volumes.rs | 34 +++--- 13 files changed, 228 insertions(+), 372 deletions(-) diff --git a/src/api/config.rs b/src/api/config.rs index 3d7a261..3f705e9 100644 --- a/src/api/config.rs +++ b/src/api/config.rs @@ -2,10 +2,9 @@ use std::io; use crate::{ config::config::{ConfigType, FileFormat, RoozCfg, SystemConfig}, - constants, model::{ types::AnyError, - volume::{RoozVolume, RoozVolumeRole}, + volume::{RoozVolume, RoozVolumeRole, VolumeFile}, }, util::labels::Labels, }; @@ -27,47 +26,47 @@ pub trait ConfigReader { } impl<'a> ConfigApi<'a> { - pub async fn store( + async fn store_config_file( &self, workspace_key: &str, - origin: &str, - body: &str, + config_type: &ConfigType, + content: &str, + labels: Option, ) -> Result<(), AnyError> { let config_vol = RoozVolume::config_data( workspace_key, "/etc/rooz", - Some( - [(ConfigType::Body.file_path().to_string(), body.to_string())] - .into_iter() - .collect(), - ), - Some(Labels::from(&[Labels::config_origin(origin)])), + labels, Some(RoozVolumeRole::WorkspaceConfig), ); self.api .volume - .ensure_mounts(&vec![config_vol], None, Some(constants::ROOT_UID)) - .await?; - Ok(()) + .write_files( + &config_vol, + &[VolumeFile::new(config_type.file_path(), content)], + None, + ) + .await } - pub async fn store_bases(&self, workspace_key: &str, body: &str) -> Result<(), AnyError> { - let config_vol = RoozVolume::config_data( + pub async fn store( + &self, + workspace_key: &str, + origin: &str, + body: &str, + ) -> Result<(), AnyError> { + self.store_config_file( workspace_key, - "/etc/rooz", - Some( - [(ConfigType::Bases.file_path().to_string(), body.to_string())] - .into_iter() - .collect(), - ), - None, - Some(RoozVolumeRole::WorkspaceConfig), - ); - self.api - .volume - .ensure_mounts(&vec![config_vol], None, Some(constants::ROOT_UID)) - .await?; - Ok(()) + &ConfigType::Body, + body, + Some(Labels::from(&[Labels::config_origin(origin)])), + ) + .await + } + + pub async fn store_bases(&self, workspace_key: &str, body: &str) -> Result<(), AnyError> { + self.store_config_file(workspace_key, &ConfigType::Bases, body, None) + .await } pub async fn read( @@ -97,25 +96,8 @@ impl<'a> ConfigApi<'a> { } pub async fn store_runtime(&self, workspace_key: &str, data: &str) -> Result<(), AnyError> { - let config_vol = RoozVolume::config_data( - workspace_key, - "/etc/rooz", - Some( - [( - ConfigType::Runtime.file_path().to_string(), - data.to_string(), - )] - .into_iter() - .collect(), - ), - None, - Some(RoozVolumeRole::WorkspaceConfig), - ); - self.api - .volume - .ensure_mounts(&vec![config_vol], None, Some(constants::ROOT_UID)) - .await?; - Ok(()) + self.store_config_file(workspace_key, &ConfigType::Runtime, data, None) + .await } fn edit_error(&self, message: &str) -> () { diff --git a/src/api/sidecar.rs b/src/api/sidecar.rs index df97bd2..d235969 100644 --- a/src/api/sidecar.rs +++ b/src/api/sidecar.rs @@ -39,16 +39,16 @@ impl<'a> WorkspaceApi<'a> { let mounts: HashMap = s.mounts.clone(); - let volumes_v2 = VolumeApi::create_volume_specs( + let volume_specs = VolumeApi::create_volume_specs( workspace_key, &config.data, &config.all_mounts(), false, ); - self.api.volume.ensure_volumes_v2(&volumes_v2).await?; + self.api.volume.ensure_volumes(&volume_specs).await?; - let mut mounts_v2 = Vec::new(); + let mut container_mounts = Vec::new(); let mounts_all = mounts .iter() @@ -58,13 +58,14 @@ impl<'a> WorkspaceApi<'a> { let mounts_config = self.api .volume - .mounts_with_sources(&volumes_v2, &mounts_all, false); + .mounts_with_sources(&volume_specs, &mounts_all, false); //TODO: not setting home dir as it depends on the user. When using uid the user might not // exist so it hard to make it work predictably. Consider marking as not supported by design - let real_mounts = VolumeApi::real_mounts_v2(mounts_config.clone(), None); + let real_mounts = VolumeApi::real_mounts(mounts_config.clone(), None); - mounts_v2.extend_from_slice(self.api.volume.mounts_v2(&real_mounts).await?.as_slice()); + container_mounts + .extend_from_slice(self.api.volume.mounts(&real_mounts).await?.as_slice()); for (t, m) in real_mounts.clone() { s.real_mounts.insert(t.clone(), m.clone()); @@ -116,7 +117,7 @@ impl<'a> WorkspaceApi<'a> { } else { Some(args.clone()) }, - mounts: Some(mounts_v2), + mounts: Some(container_mounts), ports: Some(ports), work_dir: Some(s.work_dir.as_str()), run_mode: RunMode::Sidecar, diff --git a/src/api/system_config.rs b/src/api/system_config.rs index 8d1c49c..881a27a 100644 --- a/src/api/system_config.rs +++ b/src/api/system_config.rs @@ -3,6 +3,7 @@ use std::sync::OnceLock; use crate::{ api::Api, config::config::SystemConfig, + constants, model::{types::AnyError, volume::RoozVolume}, }; @@ -12,11 +13,11 @@ 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 || true" - .into(), - Some(vec![ - RoozVolume::system_config_read("/tmp/sys").to_mount(None), - ]), + format!( + "ls /tmp/sys/{f} > /dev/null 2>&1 && cat /tmp/sys/{f} || true", + f = constants::SYSTEM_CONFIG_FILE + ), + Some(vec![RoozVolume::system_config("/tmp/sys").to_mount(None)]), None, None, ) diff --git a/src/api/volume.rs b/src/api/volume.rs index d162229..6b7951f 100644 --- a/src/api/volume.rs +++ b/src/api/volume.rs @@ -1,10 +1,9 @@ use std::collections::{HashMap, HashSet}; -use std::path::Path; use crate::config::config::{DataEntry, DataExt, DataValue, MountSource}; use crate::model::types::{ - ContentGenerator, DataEntryKey, DataEntryVolumeSpec, FileSpec, OneShotResult, TargetDir, - TargetFile, TargetPath, UserFile, VolumeFilesSpec, VolumeName, VolumeSpec, + ContentGenerator, DataEntryKey, DataEntryVolumeSpec, FileName, FileSpec, OneShotResult, + TargetDir, TargetPath, UserFile, VolumeFilesSpec, VolumeName, VolumeSpec, }; use crate::util::id; use crate::util::labels::DATA_ROLE; @@ -13,7 +12,7 @@ use crate::{ constants, model::{ types::{AnyError, VolumeResult}, - volume::{RoozVolume, RoozVolumeFile}, + volume::{RoozVolume, VolumeFile}, }, util::labels::Labels, }; @@ -26,13 +25,8 @@ use bollard::{ use bollard_stubs::models::MountType::VOLUME; use bollard_stubs::models::VolumeCreateRequest; -const SHADOW_ROOT_DIR: &str = "/var/lib/rooz"; - -struct TarFile { - path: String, - content: String, - executable: bool, -} +// where the volume gets mounted inside the populate one-shot container +const POPULATE_DIR: &str = "/var/lib/rooz"; impl<'a> VolumeApi<'a> { pub async fn get_all(&self, labels: &Labels) -> Result, AnyError> { @@ -79,7 +73,7 @@ impl<'a> VolumeApi<'a> { } } - pub async fn ensure_volume_v2(&self, spec: &VolumeSpec) -> Result { + pub async fn ensure_volume(&self, spec: &VolumeSpec) -> Result { match self.client.inspect_volume(&spec.name).await { Ok(_) => { log::debug!("Reusing an existing {} volume", &spec.name); @@ -252,7 +246,7 @@ impl<'a> VolumeApi<'a> { .collect::>() } - pub fn real_mounts_v2( + pub fn real_mounts( mounts: HashMap, home_dir: Option<&str>, ) -> HashMap { @@ -265,21 +259,15 @@ impl<'a> VolumeApi<'a> { generator, executable, .. - } => { - let shadow_file = Path::new(SHADOW_ROOT_DIR) - .join(&source_entry.data.clone().name()) - .with_extension("data"); - - ( - expanded_target.clone(), - Some(FileSpec { - target_file: TargetFile(shadow_file.to_string_lossy().to_string()), - user_file: UserFile(expanded_target), - generator, - executable, - }), - ) - } + } => ( + expanded_target.clone(), + Some(FileSpec { + file_name: FileName(source_entry.data.clone().name()), + user_file: UserFile(expanded_target), + generator, + executable, + }), + ), _ => (expanded_target, None), }; ( @@ -302,7 +290,7 @@ impl<'a> VolumeApi<'a> { }, ) } - pub async fn ensure_volumes_v2( + pub async fn ensure_volumes( &self, data_entries: &HashMap, ) -> Result, AnyError> { @@ -312,7 +300,7 @@ impl<'a> VolumeApi<'a> { .map(|(_, v)| (v.volume.name.clone(), v.volume.clone())) .collect::>() { - let volume_result = self.ensure_volume_v2(&v).await?; + let volume_result = self.ensure_volume(&v).await?; result.insert(VolumeName(k), volume_result); } Ok(result) @@ -323,9 +311,9 @@ impl<'a> VolumeApi<'a> { volume_file: VolumeFilesSpec, uid: Option, ) -> Result<(), AnyError> { - let populate_target = TargetDir(SHADOW_ROOT_DIR.to_string()); - self.ensure_file_v2( - SHADOW_ROOT_DIR, + let populate_target = TargetDir(POPULATE_DIR.to_string()); + self.ensure_files( + POPULATE_DIR, &volume_file.clone(), Self::populate_mount(&populate_target, &volume_file), uid, @@ -341,13 +329,10 @@ impl<'a> VolumeApi<'a> { target.as_str() ); - let subpath = source.files.first().map(|f| { - Path::new(f.target_file.as_str()) - .file_name() - .unwrap() - .to_string_lossy() - .into_owned() - }); + let subpath = source + .files + .first() + .map(|f| f.file_name.as_str().to_string()); Mount { target: Some(target.as_str().to_string()), @@ -372,14 +357,14 @@ impl<'a> VolumeApi<'a> { } } - pub async fn mounts_v2( + pub async fn mounts( &self, real_mounts: &HashMap, ) -> Result, AnyError> { let mut mount_entries = HashMap::new(); mount_entries.extend(real_mounts.clone()); - let mounts_v2 = mount_entries + let mounts = mount_entries .into_iter() .map(|(target, source)| Self::mount(&target, &source)) .map(|v| (v.target.clone().unwrap().to_string(), v.clone())) @@ -387,80 +372,36 @@ impl<'a> VolumeApi<'a> { .into_values() .collect::>(); - Ok(mounts_v2) - } - - pub async fn ensure_volume( - &self, - name: &str, - force_recreate: bool, - labels: Option, - ) -> Result { - let create_vol_options = VolumeCreateRequest { - name: Some(name.into()), - labels: labels.map(|x| x.into()), - ..Default::default() - }; - - match self.client.inspect_volume(&name).await { - Ok(_) if force_recreate => { - let options = RemoveVolumeOptions { force: true }; - self.client.remove_volume(&name, Some(options)).await?; - self.create_volume(create_vol_options).await - } - Ok(_) => { - log::debug!("Reusing an existing {} volume", &name); - Ok(VolumeResult::AlreadyExists) - } - Err(DockerResponseServerError { - status_code: 404, - message: _, - }) => self.create_volume(create_vol_options).await, - Err(e) => panic!("{}", e), - } + Ok(mounts) } pub async fn ensure_mounts( &self, volumes: &Vec, tilde_replacement: Option<&str>, - uid: Option<&str>, ) -> Result, AnyError> { let mut mounts = vec![]; for v in volumes { - let mount = self - .ensure_mount(&v, tilde_replacement, v.labels.clone()) - .await?; - if let RoozVolume { - path, - files: Some(files), - .. - } = v - { - self.ensure_file(&v.safe_volume_name(), path, &files, mount.clone(), uid) - .await? - }; - - mounts.push(mount); + log::debug!("Process volume: {:?}", &v); + self.ensure_volume(&v.to_spec()).await?; + mounts.push(v.to_mount(tilde_replacement)); } - Ok(mounts.clone()) + Ok(mounts) } - async fn ensure_mount( + pub async fn write_files( &self, volume: &RoozVolume, - tilde_replacement: Option<&str>, - labels: Option, - ) -> Result { - log::debug!("Process volume: {:?}", &volume); - let mount = volume.to_mount(tilde_replacement); - if let Some(name) = &mount.source { - self.ensure_volume(&name, false, labels).await?; - } - Ok(mount) + files: &[VolumeFile], + uid: Option, + ) -> Result<(), AnyError> { + let spec = volume.to_spec(); + self.ensure_volume(&spec).await?; + self.populate(&spec.name, &volume.path, files, volume.to_mount(None), uid) + .await } - fn files_tar(files: &[TarFile], uid: Option) -> Result, AnyError> { + fn files_tar(files: &[VolumeFile], uid: Option) -> Result, AnyError> { let mut builder = tar::Builder::new(Vec::new()); for f in files { let mut header = tar::Header::new_gnu(); @@ -478,7 +419,7 @@ impl<'a> VolumeApi<'a> { &self, volume_name: &str, root_dir: &str, - files: &[TarFile], + files: &[VolumeFile], mount: Mount, uid: Option, ) -> Result<(), AnyError> { @@ -512,7 +453,7 @@ impl<'a> VolumeApi<'a> { .await } - async fn ensure_file_v2( + async fn ensure_files( &self, root_dir: &str, spec: &VolumeFilesSpec, @@ -548,11 +489,8 @@ impl<'a> VolumeApi<'a> { } }; - tar_files.push(TarFile { - path: Path::new(f.target_file.as_str()) - .strip_prefix(root_dir)? - .to_string_lossy() - .into_owned(), + tar_files.push(VolumeFile { + path: f.file_name.as_str().to_string(), // IMPORTANT: never trim content so YAML multi-line strings are respected and can // control whitespace and most importantly EOLs content, @@ -563,29 +501,6 @@ impl<'a> VolumeApi<'a> { self.populate(spec.volume_name.as_str(), root_dir, &tar_files, mount, uid) .await } - - async fn ensure_file( - &self, - volume_name: &str, - parent_dir: &str, - files: &Vec, - mount: Mount, - uid: Option<&str>, - ) -> Result<(), AnyError> { - let tar_files = files - .iter() - .map(|f| TarFile { - path: f.file_path.to_string(), - content: f.data.trim().to_string(), - executable: false, - }) - .collect::>(); - - let uid = uid.map(|u| u.parse::()).transpose()?; - - self.populate(volume_name, parent_dir, &tar_files, mount, uid) - .await - } } #[cfg(test)] @@ -593,9 +508,10 @@ mod tests { use crate::api::VolumeApi; use crate::config::config::{DataEntry, DataValue, MountSource}; use crate::model::types::{ - ContentGenerator, DataEntryKey, DataEntryVolumeSpec, FileSpec, TargetDir, TargetFile, + ContentGenerator, DataEntryKey, DataEntryVolumeSpec, FileName, FileSpec, TargetDir, TargetPath, UserFile, VolumeFilesSpec, VolumeName, VolumeSpec, }; + use crate::model::volume::VolumeFile; use std::collections::HashMap; fn dir() -> DataValue { @@ -689,7 +605,7 @@ mod tests { }, ); - let real = VolumeApi::real_mounts_v2(mounts, None); + let real = VolumeApi::real_mounts(mounts, None); let entry = real.get(&TargetDir("/work".to_string())).unwrap(); assert_eq!(entry.volume_name.as_str(), "rooz-ws-work"); assert!(entry.files.is_empty()); @@ -713,7 +629,7 @@ mod tests { }, ); - let real = VolumeApi::real_mounts_v2(mounts, Some("/home/user")); + let real = VolumeApi::real_mounts(mounts, Some("/home/user")); let entry = real .get(&TargetDir("/home/user/.myconfig".to_string())) .unwrap(); @@ -721,7 +637,7 @@ mod tests { assert_eq!(entry.files.len(), 1); let f = &entry.files[0]; assert_eq!(f.user_file.as_str(), "/home/user/.myconfig"); - assert_eq!(f.target_file.as_str(), "/var/lib/rooz/myconfig.data"); + assert_eq!(f.file_name.as_str(), "myconfig"); } #[test] @@ -742,7 +658,7 @@ mod tests { }, ); - let real = VolumeApi::real_mounts_v2(mounts, None); + let real = VolumeApi::real_mounts(mounts, None); assert!( real.contains_key(&TargetDir("~/.myconfig".to_string())), "without a home dir the tilde must be left as-is" @@ -750,9 +666,9 @@ mod tests { } #[test] - fn real_mounts_shadow_path_replaces_existing_extension() { - // known wart: .with_extension("data") replaces an existing extension, - // so entries 'app.yaml' and 'app.json' collide on /var/lib/rooz/app.data + fn real_mounts_file_keeps_entry_name() { + // files keep the entry name verbatim so entries like + // 'app.yaml' and 'app.json' cannot collide let mut mounts = HashMap::new(); mounts.insert( TargetPath("/etc/app.yaml".to_string()), @@ -769,12 +685,9 @@ mod tests { }, ); - let real = VolumeApi::real_mounts_v2(mounts, None); + let real = VolumeApi::real_mounts(mounts, None); let entry = real.get(&TargetDir("/etc/app.yaml".to_string())).unwrap(); - assert_eq!( - entry.files[0].target_file.as_str(), - "/var/lib/rooz/app.data" - ); + assert_eq!(entry.files[0].file_name.as_str(), "app.yaml"); } #[test] @@ -794,7 +707,7 @@ mod tests { mounts.insert(TargetPath("/etc/file-a".to_string()), spec("file-a")); mounts.insert(TargetPath("/etc/file-b".to_string()), spec("file-b")); - let real = VolumeApi::real_mounts_v2(mounts, None); + let real = VolumeApi::real_mounts(mounts, None); assert_eq!(real.len(), 2); for entry in real.values() { assert_eq!(entry.volume_name.as_str(), "rooz-ws-inline"); @@ -841,7 +754,7 @@ mod tests { let spec = VolumeFilesSpec { volume_name: VolumeName("rooz-ws-inline".to_string()), files: vec![FileSpec { - target_file: TargetFile("/var/lib/rooz/myconfig.data".to_string()), + file_name: FileName("myconfig".to_string()), user_file: UserFile("/home/user/.myconfig".to_string()), generator: ContentGenerator::Inline("x".to_string()), executable: false, @@ -853,7 +766,7 @@ mod tests { assert_eq!(m.target.as_deref(), Some("/home/user/.myconfig")); assert_eq!(m.source.as_deref(), Some("rooz-ws-inline")); let subpath = m.volume_options.expect("volume options expected").subpath; - assert_eq!(subpath.as_deref(), Some("myconfig.data")); + assert_eq!(subpath.as_deref(), Some("myconfig")); } #[test] @@ -893,12 +806,12 @@ mod tests { #[test] fn files_tar_roundtrip() { let files = vec![ - super::TarFile { + VolumeFile { path: "plain.data".to_string(), content: "line1\n\nline3\n".to_string(), executable: false, }, - super::TarFile { + VolumeFile { path: "script.data".to_string(), content: "#!/bin/sh\necho hi\n".to_string(), executable: true, @@ -923,7 +836,7 @@ mod tests { #[test] fn files_tar_defaults_to_root_ownership() { - let files = vec![super::TarFile { + let files = vec![VolumeFile { path: "cfg".to_string(), content: "x".to_string(), executable: false, @@ -939,7 +852,7 @@ mod tests { let spec = VolumeFilesSpec { volume_name: VolumeName("rooz-ws-inline".to_string()), files: vec![FileSpec { - target_file: TargetFile("/var/lib/rooz/myconfig.data".to_string()), + file_name: FileName("myconfig".to_string()), user_file: UserFile("/home/user/.myconfig".to_string()), generator: ContentGenerator::Inline("x".to_string()), executable: false, diff --git a/src/api/workspace/create.rs b/src/api/workspace/create.rs index e0acee4..96b63e7 100644 --- a/src/api/workspace/create.rs +++ b/src/api/workspace/create.rs @@ -31,7 +31,7 @@ impl<'a> WorkspaceApi<'a> { let mut mounts = self .api .volume - .ensure_mounts(&volumes, Some(&home_dir), Some(&spec.uid)) + .ensure_mounts(&volumes, Some(&home_dir)) .await?; mounts.push(ssh::mount( diff --git a/src/cmd/init.rs b/src/cmd/init.rs index e15bd71..7979aa7 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -6,8 +6,8 @@ use crate::{ config::config::SystemConfig, constants, model::{ - types::{AnyError, VolumeResult}, - volume::{RoozVolume, RoozVolumeRole}, + types::{AnyError, VolumeResult, VolumeSpec}, + volume::{RoozVolume, RoozVolumeRole, VolumeFile}, }, util::{labels::Labels, ssh}, }; @@ -45,36 +45,38 @@ impl<'a> InitApi<'a> { Some(identity) => age::x25519::Identity::from_str(&identity)?, }; if spec.force { - self.volume - .ensure_mounts( - &vec![RoozVolume::system_config_init( - "/tmp/sys", - SystemConfig { - age_key: Some(age_key.to_string().expose_secret().to_string()), - gitconfig: Some( - r#" + let config = 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 "# - .trim() - .to_string(), - ), - }, - )?], + .trim() + .to_string(), + ), + }; + self.volume + .write_files( + &RoozVolume::system_config("/tmp/sys"), + &[VolumeFile::new( + constants::SYSTEM_CONFIG_FILE, + &SystemConfig::to_string(&config)?, + )], None, - Some(constants::ROOT_UID), ) .await?; } + // the ssh-key volume is never recreated (even on --force) as it may be + // used by existing workspaces match self .volume - .ensure_volume( - ssh::VOLUME_NAME.into(), - false, // can't really recreate the volume if it is used by workspaces without dropping the workspaces - Some(Labels::from(&[Labels::role( + .ensure_volume(&VolumeSpec { + name: ssh::VOLUME_NAME.into(), + labels: Some(Labels::from(&[Labels::role( RoozVolumeRole::SshKey.as_str(), )])), - ) + }) .await? { VolumeResult::Created { .. } => self.init_ssh(&image_id, uid).await?, diff --git a/src/cmd/new.rs b/src/cmd/new.rs index 8fd7b8f..df9b5d3 100644 --- a/src/cmd/new.rs +++ b/src/cmd/new.rs @@ -90,7 +90,7 @@ impl<'a> WorkspaceApi<'a> { .ensure(&cfg.image, cli_params.pull_image) .await?; - let volumes_v2 = + let volume_specs = VolumeApi::create_volume_specs(workspace_key, &cfg.data, &cfg.all_mounts(), true); let mounts_all = &cfg @@ -99,15 +99,15 @@ impl<'a> WorkspaceApi<'a> { .map(|(target, source)| (target.to_string(), source.resolve_key(target))) .collect::>(); - let volume_results = self.api.volume.ensure_volumes_v2(&volumes_v2).await?; + let volume_results = self.api.volume.ensure_volumes(&volume_specs).await?; let home_dir = format!("/home/{}", &cfg.user); let mounts_config = self .api .volume - .mounts_with_sources(&volumes_v2, mounts_all, true); + .mounts_with_sources(&volume_specs, mounts_all, true); - let real_mounts = VolumeApi::real_mounts_v2(mounts_config.clone(), Some(&home_dir)); + let real_mounts = VolumeApi::real_mounts(mounts_config.clone(), Some(&home_dir)); let cfg = RuntimeConfig { real_mounts: real_mounts.clone(), @@ -116,7 +116,7 @@ impl<'a> WorkspaceApi<'a> { let mut cfg2 = cfg.clone(); - let mounts_v2 = self.api.volume.mounts_v2(&real_mounts).await?; + let container_mounts = self.api.volume.mounts(&real_mounts).await?; for (_, m) in real_mounts.clone() { //TODO: when initializing volumes both here in sidecars we should verify // if each file exists and if not create them @@ -190,7 +190,7 @@ impl<'a> WorkspaceApi<'a> { }) .as_ref() .map(|x| x.iter().map(|z| z.as_ref()).collect()), - mounts: mounts_v2, + mounts: container_mounts, install: cfg2.install, ..*work_spec }; diff --git a/src/constants.rs b/src/constants.rs index a341c4f..ca815c7 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -7,6 +7,7 @@ pub const ROOT_UID: &'static str = "0"; pub const ROOT_UID_INT: i32 = 0; pub const ROOT_USER: &'static str = "root"; pub const WORK_DIR: &'static str = "/work"; +pub const SYSTEM_CONFIG_FILE: &'static str = "rooz.config"; pub fn default_command<'a>() -> Option> { Some(vec!["cat"]) } diff --git a/src/main.rs b/src/main.rs index f733708..5d57275 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,10 @@ use rooz::{ StopParams, TmpParams, }, cmd::remote, - model::{types::AnyError, volume::RoozVolume}, + model::{ + types::AnyError, + volume::{RoozVolume, VolumeFile}, + }, util::backend::{ContainerBackend, check_version_floor}, }; @@ -371,10 +374,13 @@ async fn main() -> Result<(), AnyError> { .system_edit_string(rooz.get_system_config_string().await?.clone()) .await?; volume_api - .ensure_mounts( - &vec![RoozVolume::system_config("/tmp/sys", config_string)], + .write_files( + &RoozVolume::system_config("/tmp/sys"), + &[VolumeFile::new( + constants::SYSTEM_CONFIG_FILE, + &config_string, + )], None, - Some(constants::ROOT_UID), ) .await?; } diff --git a/src/model/types.rs b/src/model/types.rs index a13b945..42dc37d 100644 --- a/src/model/types.rs +++ b/src/model/types.rs @@ -203,15 +203,15 @@ impl From for TargetDir { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct TargetFile(pub String); +pub struct FileName(pub String); -impl TargetFile { +impl FileName { pub fn as_str(&self) -> &str { &self.0 } } -impl From for TargetFile { +impl From for FileName { fn from(s: String) -> Self { Self(s) } @@ -273,7 +273,7 @@ pub enum ContentGenerator { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FileSpec { - pub target_file: TargetFile, + pub file_name: FileName, pub user_file: UserFile, pub generator: ContentGenerator, pub executable: bool, diff --git a/src/model/volume.rs b/src/model/volume.rs index ef4c391..8ff62b8 100644 --- a/src/model/volume.rs +++ b/src/model/volume.rs @@ -1,8 +1,5 @@ -use std::collections::HashMap; - use crate::{ - config::config::SystemConfig, - model::types::AnyError, + model::types::VolumeSpec, util::{ id::sanitize, labels::{ @@ -43,16 +40,28 @@ impl RoozVolumeRole { } #[derive(Clone)] -pub struct RoozVolumeFile { - pub file_path: String, - pub data: String, +pub struct VolumeFile { + pub path: String, + pub content: String, + pub executable: bool, } -impl std::fmt::Debug for RoozVolumeFile { +impl VolumeFile { + pub fn new(path: &str, content: &str) -> VolumeFile { + VolumeFile { + path: path.to_string(), + content: content.to_string(), + executable: false, + } + } +} + +impl std::fmt::Debug for VolumeFile { 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())) + f.debug_struct("VolumeFile") + .field("path", &self.path) + .field("content", &format!("<{} bytes>", self.content.len())) + .field("executable", &self.executable) .finish() } } @@ -62,7 +71,6 @@ pub struct RoozVolume { pub path: String, pub role: RoozVolumeRole, pub sharing: RoozVolumeSharing, - pub files: Option>, pub labels: Option, } @@ -115,12 +123,18 @@ impl RoozVolume { } } + pub fn to_spec(&self) -> VolumeSpec { + VolumeSpec { + name: self.safe_volume_name(), + labels: self.labels.clone(), + } + } + pub fn work(key: &str, path: &str) -> RoozVolume { RoozVolume { path: path.into(), sharing: RoozVolumeSharing::Exclusive { key: key.into() }, role: RoozVolumeRole::Work, - files: None, labels: Some(Labels::from(&[ Labels::workspace(key), Labels::role(RoozVolumeRole::Work.as_str()), @@ -133,7 +147,6 @@ impl RoozVolume { path: path.into(), sharing: RoozVolumeSharing::Shared, role: RoozVolumeRole::Cache, - files: None, labels: Some(Labels::from(&[Labels::role( RoozVolumeRole::Cache.as_str(), )])), @@ -143,7 +156,6 @@ impl RoozVolume { pub fn config_data( workspace_key: &str, path: &str, - files: Option>, labels: Option, role: Option, ) -> RoozVolume { @@ -156,33 +168,13 @@ impl RoozVolume { if let Some(items) = labels { all_labels.extend_with_labels(items); } - match files { - Some(files) => RoozVolume { - path: path.to_string(), - role: role, - sharing: RoozVolumeSharing::Exclusive { - key: workspace_key.into(), - }, - files: Some( - files - .iter() - .map(|(file_name, data)| RoozVolumeFile { - file_path: file_name.to_string(), - data: data.to_string(), - }) - .collect::>(), - ), - labels: Some(all_labels), - }, - None => RoozVolume { - path: path.into(), - role, - sharing: RoozVolumeSharing::Exclusive { - key: workspace_key.into(), - }, - files: None, - labels: Some(all_labels), + RoozVolume { + path: path.into(), + role, + sharing: RoozVolumeSharing::Exclusive { + key: workspace_key.into(), }, + labels: Some(all_labels), } } @@ -193,44 +185,20 @@ impl RoozVolume { key: workspace_key.to_string(), }, role: RoozVolumeRole::WorkspaceConfig, - files: None, labels: None, } } - pub fn system_config_read(path: &str) -> RoozVolume { + pub fn system_config(path: &str) -> RoozVolume { RoozVolume { path: path.into(), sharing: RoozVolumeSharing::Shared, role: RoozVolumeRole::SystemConfig, - files: None, labels: Some(Labels::from(&[Labels::role( RoozVolumeRole::SystemConfig.as_str(), )])), } } - - pub fn system_config(path: &str, data: String) -> RoozVolume { - RoozVolume { - path: path.into(), - sharing: RoozVolumeSharing::Shared, - role: RoozVolumeRole::SystemConfig, - files: Some(vec![RoozVolumeFile { - file_path: "rooz.config".to_string(), - data: data, - }]), - labels: Some(Labels::from(&[Labels::role( - RoozVolumeRole::SystemConfig.as_str(), - )])), - } - } - - pub fn system_config_init(path: &str, data: SystemConfig) -> Result { - Ok(RoozVolume::system_config( - path, - SystemConfig::to_string(&data)?, - )) - } } #[cfg(test)] @@ -242,7 +210,6 @@ mod tests { path: path.to_string(), role, sharing, - files: None, labels: None, } } @@ -340,44 +307,34 @@ mod tests { } #[test] - fn config_data_builds_files() { - let mut files = HashMap::new(); - files.insert( - "workspace.config".to_string(), - "image: alpine\n".to_string(), - ); - + fn config_data_workspace_config_name() { let v = RoozVolume::config_data( "ws", "/etc/rooz", - Some(files), None, Some(RoozVolumeRole::WorkspaceConfig), ); - assert_eq!(v.path, "/etc/rooz"); assert_eq!(v.safe_volume_name(), "rooz-ws-workspace-config"); - let files = v.files.expect("files expected"); - assert_eq!(files.len(), 1); - assert_eq!(files[0].file_path, "workspace.config"); - // data is stored verbatim here; trimming happens later in ensure_file - assert_eq!(files[0].data, "image: alpine\n"); } #[test] - fn config_data_without_files() { - let v = RoozVolume::config_data("ws", "/etc/rooz", None, None, None); - assert!(v.files.is_none()); + fn config_data_default_role_name() { + let v = RoozVolume::config_data("ws", "/etc/rooz", None, None); assert_eq!(v.safe_volume_name(), "rooz_ws_-etc-rooz_data"); } #[test] - fn system_config_carries_rooz_config_file() { - let v = RoozVolume::system_config("/tmp/sys", "cfg-body".to_string()); + fn system_config_name() { + let v = RoozVolume::system_config("/tmp/sys"); assert_eq!(v.safe_volume_name(), "rooz_sys-config"); - let files = v.files.expect("files expected"); - assert_eq!(files.len(), 1); - assert_eq!(files[0].file_path, "rooz.config"); - assert_eq!(files[0].data, "cfg-body"); + } + + #[test] + fn to_spec_carries_name_and_labels() { + let v = RoozVolume::system_config("/tmp/sys"); + let spec = v.to_spec(); + assert_eq!(spec.name, "rooz_sys-config"); + assert!(spec.labels.is_some()); } } diff --git a/src/util/git.rs b/src/util/git.rs index 0a56d78..49472b9 100644 --- a/src/util/git.rs +++ b/src/util/git.rs @@ -1,5 +1,4 @@ use gix_config::File; -use std::collections::HashMap; use crate::{ api::{GitApi, config::ConfigBody, container}, @@ -7,7 +6,7 @@ use crate::{ constants, model::{ types::{AnyError, ContainerResult, RunMode, RunSpec}, - volume::RoozVolume, + volume::{RoozVolume, VolumeFile}, }, }; @@ -143,24 +142,22 @@ impl<'a> GitApi<'a> { let mut volumes: Vec = vec![]; if let Some(gitconfig) = &self.api.get_system_config().await?.gitconfig { - let mut config_hashmap = HashMap::::new(); - config_hashmap.insert(".gitconfig".into(), gitconfig.to_string()); - let git_config_vol = RoozVolume::config_data( - &spec.workspace_key, - "/tmp/rooz/", - Some(config_hashmap), - None, - None, - ); - volumes.push(git_config_vol.clone()); + let git_config_vol = + RoozVolume::config_data(&spec.workspace_key, "/tmp/rooz/", None, None); + self.api + .volume + .write_files( + &git_config_vol, + &[VolumeFile::new(".gitconfig", gitconfig)], + Some(spec.uid.parse::()?), + ) + .await?; + volumes.push(git_config_vol); } volumes.push(RoozVolume::work(&spec.workspace_key, &spec.working_dir)); - self.api - .volume - .ensure_mounts(&volumes, None, Some(&spec.uid)) - .await?; + self.api.volume.ensure_mounts(&volumes, None).await?; for vol in &volumes { mounts.push(vol.to_mount(None)); diff --git a/tests/volumes.rs b/tests/volumes.rs index efb36bb..05ca667 100644 --- a/tests/volumes.rs +++ b/tests/volumes.rs @@ -203,8 +203,8 @@ async fn inline_data_content_written_to_volume() { data_vol ); - // Content is written at greeting.data inside the volume (shadow-path convention). - let content = env.volume_file(&data_vol, "greeting.data").await; + // Content is written under the entry name inside the volume. + let content = env.volume_file(&data_vol, "greeting").await; assert_eq!( content.trim(), "hello from rooz", @@ -255,14 +255,14 @@ async fn inline_mounts_share_inline_volume() { new_workspace(&env, &key, &cfg_path); // Both inline mounts land in the shared inline volume; file names are - // sanitized target paths (shadow-path convention). + // sanitized target paths. let inline_vol = format!("rooz-{}-inline", key); assert_eq!( - env.volume_file(&inline_vol, "---cfg-a.data").await, + env.volume_file(&inline_vol, "---cfg-a").await, "content-a\n" ); assert_eq!( - env.volume_file(&inline_vol, "---cfg-b.data").await, + env.volume_file(&inline_vol, "---cfg-b").await, "content-b\n" ); @@ -287,11 +287,8 @@ async fn data_file_modes_ownership_and_eols() { // executable entries are 755, plain files 644; owner is the workspace uid // (default 1000) - assert_eq!( - env.volume_stat(&script_vol, "script.data").await, - "755 1000" - ); - assert_eq!(env.volume_stat(&plain_vol, "plain.data").await, "644 1000"); + assert_eq!(env.volume_stat(&script_vol, "script").await, "755 1000"); + assert_eq!(env.volume_stat(&plain_vol, "plain").await, "644 1000"); // the volume root dir must be workspace-user writable assert!( @@ -301,7 +298,7 @@ async fn data_file_modes_ownership_and_eols() { // content must round-trip byte-exact: empty lines and the trailing EOL preserved assert_eq!( - env.volume_file(&plain_vol, "plain.data").await, + env.volume_file(&plain_vol, "plain").await, "line1\n\nline3\n" ); @@ -322,7 +319,7 @@ async fn generated_data_file_content() { new_workspace(&env, &key, &cfg_path); let gen_vol = format!("rooz-{}-gen", key); - assert_eq!(env.volume_file(&gen_vol, "gen.data").await, "gen-output"); + assert_eq!(env.volume_file(&gen_vol, "gen").await, "gen-output"); cleanup(&env, &key, &cfg_path); } @@ -341,7 +338,7 @@ async fn generated_multiline_data_file() { new_workspace(&env, &key, &cfg_path); let vol = format!("rooz-{}-genml", key); - let content = env.volume_file(&vol, "genml.data").await; + let content = env.volume_file(&vol, "genml").await; // generated content must round-trip byte-exact, same as inline content assert_eq!(content, "l1\nl2\n"); @@ -362,7 +359,7 @@ async fn generated_data_file_excludes_stderr() { new_workspace(&env, &key, &cfg_path); let vol = format!("rooz-{}-gen", key); - assert_eq!(env.volume_file(&vol, "gen.data").await, "clean-output"); + assert_eq!(env.volume_file(&vol, "gen").await, "clean-output"); cleanup(&env, &key, &cfg_path); } @@ -381,7 +378,7 @@ async fn sidecar_mount_populates_data_volume() { new_workspace(&env, &key, &cfg_path); let vol = format!("rooz-{}-svc-cfg", key); - assert_eq!(env.volume_file(&vol, "svc-cfg.data").await, "svc-config\n"); + assert_eq!(env.volume_file(&vol, "svc-cfg").await, "svc-config\n"); cleanup(&env, &key, &cfg_path); } @@ -403,7 +400,7 @@ async fn large_generated_data_file() { new_workspace(&env, &key, &cfg_path); let vol = format!("rooz-{}-big", key); - let content = env.volume_file(&vol, "big.data").await; + let content = env.volume_file(&vol, "big").await; assert_eq!(content.len(), 200000); assert!(content.chars().all(|c| c == 'x')); @@ -432,8 +429,7 @@ async fn workspace_config_volume_stores_body() { let vol = format!("rooz-{}-workspace-config", key); let stored = env.volume_file(&vol, "workspace.config").await; - // pins current v1 behavior: the body is trimmed before storing - assert_eq!(stored, body.trim()); + assert_eq!(stored, body, "config body must be stored byte-exact"); let stat = env.volume_stat(&vol, "workspace.config").await; assert_eq!( @@ -488,7 +484,7 @@ async fn large_workspace_config_body() { let vol = format!("rooz-{}-workspace-config", key); let stored = env.volume_file(&vol, "workspace.config").await; - assert_eq!(stored, body.trim()); + assert_eq!(stored, body); cleanup(&env, &key, &cfg_path); }