From 61c7ffd7c2b718b1ed0b1ec8839b44c9a5161776 Mon Sep 17 00:00:00 2001 From: queil Date: Fri, 13 Mar 2026 21:46:46 +0100 Subject: [PATCH 1/4] add: generated files (volume population) --- Cargo.lock | 62 +++++++++++++++++++++---------- examples/volume-v2.yaml | 10 +++++ src/api/config.rs | 1 + src/api/container.rs | 47 +++++++++++++++++++----- src/api/system_config.rs | 1 + src/api/volume.rs | 79 ++++++++++++++++++++++++++-------------- src/config/config.rs | 48 ++++++++++++++++++------ src/main.rs | 8 +++- src/model/types.rs | 11 +++++- 9 files changed, 199 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2b3c74..07d361e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,7 +80,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", - "anstyle-parse", + "anstyle-parse 0.2.7", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse 1.0.0", "anstyle-query", "anstyle-wincon", "colorchoice", @@ -90,9 +105,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -103,6 +118,15 @@ dependencies = [ "utf8parse", ] +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + [[package]] name = "anstyle-query" version = "1.1.5" @@ -355,9 +379,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -365,11 +389,11 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ - "anstream", + "anstream 1.0.0", "anstyle", "clap_lex", "strsim", @@ -377,18 +401,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.66" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c757a3b7e39161a4e56f9365141ada2a6c915a8622c408ab6bb4b5d047371031" +checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -398,15 +422,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" @@ -682,7 +706,7 @@ version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" dependencies = [ - "anstream", + "anstream 0.6.21", "anstyle", "env_filter", "jiff", @@ -1935,9 +1959,9 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" diff --git a/examples/volume-v2.yaml b/examples/volume-v2.yaml index 02da897..5a6678b 100644 --- a/examples/volume-v2.yaml +++ b/examples/volume-v2.yaml @@ -7,12 +7,22 @@ data: [url "git@github.com:"] insteadOf = gh: + + cert: +# generate runs the command in an isolated one-shot container and captures output + generate: | + openssl genrsa -out cert.key 2048 + cat cert.key + image: docker.io/alpine/openssl + + # files included from paths are non-trivial at the moment for config originating from git # rather trivial for local files, but I'd rather provide a consistent implementation #local-file: # path: ./local.yaml mounts: + ~/certs/my.key: cert ~/placeholder: empty-dir /work: work ~/.gitconfig: git-config diff --git a/src/api/config.rs b/src/api/config.rs index 0894147..ab644cb 100644 --- a/src/api/config.rs +++ b/src/api/config.rs @@ -60,6 +60,7 @@ impl<'a> ConfigApi<'a> { RoozVolume::workspace_config_read(workspace_key, "/etc/rooz").to_mount(None), ]), None, + None, ) .await?; Ok(result.data.to_string()) diff --git a/src/api/container.rs b/src/api/container.rs index 5ae03b5..f37977e 100644 --- a/src/api/container.rs +++ b/src/api/container.rs @@ -26,8 +26,9 @@ use bollard::{ }; use crate::model::types::{TargetDir, VolumeFilesSpec}; -use bollard_stubs::query_parameters::UploadToContainerOptions; -use futures::{StreamExt, future}; +use bollard_stubs::models::MountTypeEnum; +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}; @@ -487,19 +488,29 @@ timeout $TIMEOUT sh -c 'read _ < /tmp/exec_start' || exit 1 echo "Exec session started" read _ < /tmp/exec_end -echo "Exec session ended" -exit 0"#; +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 entrypoint = epv.iter().map(String::as_str).collect(); + let work_dir = "/tmp/one-shot"; let id = self .create(RunSpec { reason: name, image: image.unwrap_or(constants::DEFAULT_IMAGE), container_name: &id::random_suffix("one-shot"), command: Some(entrypoint), - mounts, + mounts: mounts.map(|mut x| { + x.extend_from_slice(&[Mount { + target: Some(work_dir.into()), + typ: Some(MountTypeEnum::TMPFS), + ..Default::default() + }]); + x + }), uid: uid.unwrap_or(constants::ROOT_UID), + work_dir: Some(work_dir), ..Default::default() }) .await @@ -540,14 +551,13 @@ exit 0"#; .await; }); } - Ok(id) } fn format_cmd(command: String) -> Vec { let cmd = format!( r#"#!/bin/sh -trap 'echo end > /tmp/exec_end' EXIT +trap 'echo $? > /tmp/exec_exit; echo end > /tmp/exec_end' EXIT echo start > /tmp/exec_start {} "#, @@ -562,11 +572,30 @@ echo start > /tmp/exec_start command: String, mounts: Option>, uid: Option<&str>, + image: Option<&str>, ) -> Result { - let id = self.make_one_shot(name, mounts, uid, None).await?; + let id = self.make_one_shot(name, mounts, uid, image).await?; let cmd = Self::format_cmd(command); let cmd = cmd.iter().map(|x| x.as_str()).collect::>(); - let data = self.exec.output(name, &id.clone(), uid, Some(cmd)).await?; + + let id_clone = id.clone(); + let client = self.client.clone(); + + let wait_handle = tokio::spawn(async move { + client + .wait_container(&id_clone, None::) + .try_collect::>() + .await + }); + + let data = self.exec.output(name, &id, uid, Some(cmd)).await?; + let _ = match wait_handle.await? { + Ok(r) => r, + Err(Error::DockerContainerWaitError { code, .. }) => { + return Err(format!("One-shot cmd failed (exit {}): \n{}", code, data).into()); + } + Err(e) => return Err(e.into()), + }; Ok(OneShotResult { data }) } diff --git a/src/api/system_config.rs b/src/api/system_config.rs index 270ae14..adcbe3f 100644 --- a/src/api/system_config.rs +++ b/src/api/system_config.rs @@ -18,6 +18,7 @@ impl<'a> Api<'a> { RoozVolume::system_config_read("/tmp/sys").to_mount(None), ]), None, + None, ) .await?; diff --git a/src/api/volume.rs b/src/api/volume.rs index be501c3..80076ac 100644 --- a/src/api/volume.rs +++ b/src/api/volume.rs @@ -3,8 +3,8 @@ use std::path::Path; use crate::config::config::{DataEntry, DataExt, DataValue, MountSource}; use crate::model::types::{ - DataEntryKey, DataEntryVolumeSpec, FileSpec, TargetDir, TargetFile, TargetPath, UserFile, - VolumeFilesSpec, VolumeName, VolumeSpec, + ContentGenerator, DataEntryKey, DataEntryVolumeSpec, FileSpec, OneShotResult, TargetDir, + TargetFile, TargetPath, UserFile, VolumeFilesSpec, VolumeName, VolumeSpec, }; use crate::util::id; use crate::util::labels::DATA_ROLE; @@ -255,7 +255,7 @@ impl<'a> VolumeApi<'a> { let expanded_target = Self::expand_home(target.as_str().to_string(), home_dir); let (real_target, maybe_file) = match source_entry.data.clone() { DataEntry::File { - content, + generator, executable, .. } => { @@ -273,7 +273,7 @@ impl<'a> VolumeApi<'a> { Some(FileSpec { target_file: TargetFile(shadow_file.to_string_lossy().into_owned()), user_file: UserFile(expanded_target), - content: content.to_string(), + generator, executable, }), ) @@ -435,30 +435,55 @@ impl<'a> VolumeApi<'a> { mount: Mount, uid: Option, ) -> Result<(), AnyError> { - let mut cmd = spec - .files - .iter() - .map(|f| { - let parent_dir = Path::new(f.target_file.as_str()) - .parent() - .unwrap() - .to_string_lossy() - .into_owned(); - - format!( - "mkdir -p {} && echo '{}' | base64 -d > {}{}", - parent_dir, - general_purpose::STANDARD.encode(f.content.trim()), - f.target_file.as_str(), - if f.executable { - format!(" && chmod +x {}", f.target_file.as_str()) - } else { - "".to_string() + let mut cmds = Vec::new(); + for f in &spec.files { + let parent_dir = Path::new(f.target_file.as_str()) + .parent() + .unwrap() + .to_string_lossy() + .into_owned(); + + let content = match &f.generator { + ContentGenerator::Inline(content) => content.to_string(), + ContentGenerator::Script { script, image } => { + match self + .container + .one_shot_output( + &format!("generate file: {}", f.user_file.as_str()), + script.to_string(), + None, + None, + image.as_deref(), + ) + .await + { + Ok(OneShotResult { data }) => data, + Err(e) => { + return Err(format!( + "Failed generating file: {}\n{}", + f.user_file.as_str(), + e + ) + .into()); + } } - ) - }) - .collect::>() - .join(" && ".into()); + } + }; + + cmds.push(format!( + "mkdir -p {} && echo '{}' | base64 -d > {}{}", + parent_dir, + general_purpose::STANDARD.encode(content.trim()), + f.target_file.as_str(), + if f.executable { + format!(" && chmod +x {}", f.target_file.as_str()) + } else { + "".to_string() + } + )); + } + + let mut cmd = cmds.join(" && ".into()); if let Some(uid) = uid && uid != constants::ROOT_UID_INT diff --git a/src/config/config.rs b/src/config/config.rs index 9282ac9..9ee9319 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -1,4 +1,5 @@ -use crate::model::types::{AnyError, DataEntryKey}; +use crate::model::types::ContentGenerator::{Inline, Script}; +use crate::model::types::{AnyError, ContentGenerator, DataEntryKey}; use crate::util::id; use crate::{cli::WorkParams, constants}; use colored::Colorize; @@ -378,24 +379,44 @@ impl SystemConfig { #[serde(deny_unknown_fields)] pub enum DataValue { Dir {}, - InlineContent { content: String }, - InlineScript { script: String }, + InlineContent { + content: String, + #[serde(default)] + executable: bool, + }, + GeneratedContent { + generate: String, + #[serde(skip_serializing_if = "Option::is_none")] + image: Option, + #[serde(default)] + executable: bool, + }, } impl DataValue { pub fn into_entry(self, name: String) -> DataEntry { match self { - DataValue::InlineContent { content } => DataEntry::File { - name, + DataValue::InlineContent { content, - executable: false, + executable, + } => DataEntry::File { + name, + generator: Inline(content), + executable, }, - DataValue::Dir {} => DataEntry::Dir { name }, - DataValue::InlineScript { script } => DataEntry::File { + DataValue::GeneratedContent { + generate, + image, + executable, + } => DataEntry::File { name, - content: script, - executable: true, + generator: Script { + script: generate, + image, + }, + executable, }, + DataValue::Dir {} => DataEntry::Dir { name }, } } } @@ -424,7 +445,7 @@ pub enum DataEntry { }, File { name: String, - content: String, + generator: ContentGenerator, executable: bool, }, } @@ -471,6 +492,11 @@ data: empty-file: content: "" + generated-file: + generate: | + echo -n "new content" + + "#; let config: RoozCfg = serde_yaml::from_str(yaml).unwrap(); diff --git a/src/main.rs b/src/main.rs index 9532f58..da1021e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,9 +37,15 @@ use config::config::{ConfigPath, ConfigSource, FileFormat}; use util::labels::{self, Labels}; #[tokio::main] -async fn main() -> Result<(), AnyError> { +async fn main() { env_logger::init(); + if let Err(e) = run().await { + eprintln!("Error: {}", e); + std::process::exit(1); + } +} +async fn run() -> Result<(), AnyError> { log::debug!("Started"); let args = Cli::parse(); diff --git a/src/model/types.rs b/src/model/types.rs index 21348dd..6a3feac 100644 --- a/src/model/types.rs +++ b/src/model/types.rs @@ -261,11 +261,20 @@ impl From for VolumeName { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ContentGenerator { + Inline(String), + Script { + script: String, + image: Option, + }, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FileSpec { pub target_file: TargetFile, pub user_file: UserFile, - pub content: String, + pub generator: ContentGenerator, pub executable: bool, } From 94036c00902184d91eedc748c0a2a2d38a6fb290 Mon Sep 17 00:00:00 2001 From: queil Date: Fri, 13 Mar 2026 21:48:08 +0100 Subject: [PATCH 2/4] chore: style --- src/api/container.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/container.rs b/src/api/container.rs index f37977e..85fd351 100644 --- a/src/api/container.rs +++ b/src/api/container.rs @@ -501,13 +501,13 @@ 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 x| { - x.extend_from_slice(&[Mount { + mounts: mounts.map(|mut m| { + m.extend_from_slice(&[Mount { target: Some(work_dir.into()), typ: Some(MountTypeEnum::TMPFS), ..Default::default() }]); - x + m }), uid: uid.unwrap_or(constants::ROOT_UID), work_dir: Some(work_dir), From 624084ac23159bfb7414f24597f758dc535ccf19 Mon Sep 17 00:00:00 2001 From: queil Date: Fri, 13 Mar 2026 21:51:58 +0100 Subject: [PATCH 3/4] fix: test --- src/config/config.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/config/config.rs b/src/config/config.rs index 9ee9319..de5b011 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -509,9 +509,20 @@ data: DataEntry::Dir { name } => { assert_eq!(name, "some-dir"); } - DataEntry::File { name, .. } => { + DataEntry::File { + name, + generator: Inline(_), + .. + } => { assert!(name == "inline-file" || name == "empty-file"); } + DataEntry::File { + name, + generator: Script { script: _, .. }, + .. + } => { + assert_eq!(name, "generated-file"); + } } } } From cea1f636f4f9903690115e64938b3da8241c22f8 Mon Sep 17 00:00:00 2001 From: queil Date: Fri, 13 Mar 2026 21:54:38 +0100 Subject: [PATCH 4/4] fxi: test --- src/config/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config.rs b/src/config/config.rs index de5b011..31740bf 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -502,7 +502,7 @@ data: let config: RoozCfg = serde_yaml::from_str(yaml).unwrap(); let entries = config.data.unwrap().into_entries(); - assert_eq!(entries.len(), 3); + assert_eq!(entries.len(), 4); for entry in &entries { match entry {