From 316979248c2f5109a9f723ef263da0d35633b3a4 Mon Sep 17 00:00:00 2001 From: Mark Saward Date: Tue, 17 Feb 2026 21:21:42 +1100 Subject: [PATCH] fix to use pinned read_file when pinned --- .../content/docs/guides/manage-databases.md | 15 ++++++ src/store/mod.rs | 10 ++-- src/store/pinner/latest.rs | 6 +-- src/store/pinner/mod.rs | 10 +++- src/store/pinner/spawn.rs | 5 +- src/template.rs | 46 +++++++++++++++++++ 6 files changed, 81 insertions(+), 11 deletions(-) diff --git a/docs/src/content/docs/guides/manage-databases.md b/docs/src/content/docs/guides/manage-databases.md index bdc5575..f841407 100644 --- a/docs/src/content/docs/guides/manage-databases.md +++ b/docs/src/content/docs/guides/manage-databases.md @@ -158,6 +158,21 @@ command = { 4. **Reuse**: This final command is cached and reused for all SQL operations in that spawn session. +### Avoiding Repeated SSH Passphrase Prompts + +When using SSH keys with a passphrase, each connection to the database will prompt for the passphrase. Since spawn opens multiple SSH sessions during a single `apply` run, this gets tedious quickly. + +To avoid this, add your key to the SSH agent before running spawn: + +```bash +# macOS +ssh-add ~/.ssh/your_key + +# Linux +eval "$(ssh-agent -s)" +ssh-add ~/.ssh/your_key +``` + ### Complete Example: Multiple Environments ```toml diff --git a/src/store/mod.rs b/src/store/mod.rs index afc172f..619c877 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -125,10 +125,14 @@ impl Store { Ok(res) } + pub async fn load_component_bytes(&self, name: &str) -> Result>> { + self.pinner.load_bytes(name, &self.fs).await + } + pub async fn read_file_bytes(&self, path: &str) -> Result> { - let full_path = format!("{}/{}", self.pather.components_folder(), path); - let result = self.fs.read(&full_path).await?; - Ok(result.to_bytes().to_vec()) + self.load_component_bytes(path) + .await? + .ok_or_else(|| anyhow::anyhow!("file not found in components: {}", path)) } pub async fn load_migration(&self, name: &str) -> Result { diff --git a/src/store/pinner/latest.rs b/src/store/pinner/latest.rs index 2ae18f9..fc2fae9 100644 --- a/src/store/pinner/latest.rs +++ b/src/store/pinner/latest.rs @@ -25,15 +25,13 @@ impl Latest { #[async_trait] impl Pinner for Latest { /// Returns the file from the live file system if it exists. - async fn load(&self, name: &str, object_store: &Operator) -> Result> { + async fn load_bytes(&self, name: &str, object_store: &Operator) -> Result>> { let path_str = format!("{}/components/{}", self.store_path, name); let get_result = object_store.read(&path_str).await?; let bytes = get_result.to_bytes(); - let result = Ok::, object_store::Error>(bytes.to_vec()); - let res = result.map(|bytes| String::from_utf8(bytes).ok())?; - Ok(res) + Ok(Some(bytes.to_vec())) } async fn snapshot(&mut self, _object_store: &Operator) -> Result { diff --git a/src/store/pinner/mod.rs b/src/store/pinner/mod.rs index 3251f6c..b4ada6f 100644 --- a/src/store/pinner/mod.rs +++ b/src/store/pinner/mod.rs @@ -13,7 +13,15 @@ pub mod spawn; #[async_trait] pub trait Pinner: Debug + Send + Sync { - async fn load(&self, name: &str, fs: &Operator) -> Result>; + async fn load_bytes(&self, name: &str, fs: &Operator) -> Result>>; + + async fn load(&self, name: &str, fs: &Operator) -> Result> { + match self.load_bytes(name, fs).await? { + Some(bytes) => Ok(Some(String::from_utf8(bytes)?)), + None => Ok(None), + } + } + async fn snapshot(&mut self, fs: &Operator) -> Result; } diff --git a/src/store/pinner/spawn.rs b/src/store/pinner/spawn.rs index 3f71fcc..13bd747 100644 --- a/src/store/pinner/spawn.rs +++ b/src/store/pinner/spawn.rs @@ -89,7 +89,7 @@ impl Spawn { #[async_trait] impl Pinner for Spawn { /// Returns the file from the store if it exists. - async fn load(&self, name: &str, object_store: &Operator) -> Result> { + async fn load_bytes(&self, name: &str, object_store: &Operator) -> Result>> { // Borrow files from inside self.files, if not none: let files = self .files @@ -100,8 +100,7 @@ impl Pinner for Spawn { match object_store.read(path).await { Ok(get_result) => { let bytes = get_result.to_bytes(); - let contents = String::from_utf8(bytes.to_vec())?; - Ok(Some(contents)) + Ok(Some(bytes.to_vec())) } Err(_) => Ok(None), } diff --git a/src/template.rs b/src/template.rs index 8abd905..a0e8098 100644 --- a/src/template.rs +++ b/src/template.rs @@ -608,4 +608,50 @@ mod tests { let result = tmpl.render(context!()); assert!(result.is_err()); } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_read_file_filter_uses_pinned_store() { + use crate::config::FolderPather; + use crate::store::pinner::snapshot; + use crate::store::pinner::spawn::Spawn; + use opendal::services::Memory; + use opendal::Operator; + + let mem_service = Memory::default(); + let op = Operator::new(mem_service).unwrap().finish(); + + // Write a file into the components folder and snapshot it into the pinned store + op.write("components/test.txt", "pinned content") + .await + .unwrap(); + let root_hash = snapshot(&op, "pinned/", "components/").await.unwrap(); + + // Delete the original file so it only exists in the pinned CAS store + op.delete("components/test.txt").await.unwrap(); + + // Create a Spawn pinner using the snapshot hash + let pinner = Spawn::new_with_root_hash( + "pinned/".to_string(), + "components/".to_string(), + &root_hash, + &op, + ) + .await + .unwrap(); + + let pather = FolderPather { + spawn_folder: "".to_string(), + }; + let store = Store::new(Box::new(pinner), op, pather).unwrap(); + + let mut env = template_env(store, &EngineType::PostgresPSQL).unwrap(); + env.add_template( + "test.sql", + r#"{{ "test.txt"|read_file|to_string_lossy|safe }}"#, + ) + .unwrap(); + let tmpl = env.get_template("test.sql").unwrap(); + let result = tmpl.render(context!()).unwrap(); + assert_eq!(result, "pinned content"); + } }