use anyhow::Result; use super::{Argv, Cmd}; // sh -c, the only way to reach a shell feature inside a container pub struct Sh(String); impl Sh { pub fn new(script: impl Into) -> Self { Self(script.into()) } } impl Cmd for Sh { fn argv(&self) -> Argv { // -c has no long form Argv::new("sh").arg("-c").arg(&self.0) } } // commands joined by pipes, which only a shell can run pub struct Pipeline { stages: Vec, pipefail: bool, } impl Pipeline { pub fn starting(first: String) -> Self { Self { stages: vec![first], pipefail: true, } } pub fn pipe(mut self, next: &dyn Cmd) -> Result { self.stages.push(next.shell()?); Ok(self) } // for a pipeline whose last stage closes the pipe on purpose, where the // SIGPIPE that kills an earlier stage is the expected end and not a failure pub fn allow_early_close(mut self) -> Self { self.pipefail = false; self } fn script(&self) -> String { let piped = self.stages.join(" | "); // a pipeline reports only its last stage's status, so a first stage that // dies mid-stream reads as success without this if self.pipefail { format!("set -o pipefail; {piped}") } else { piped } } } impl Cmd for Pipeline { fn argv(&self) -> Argv { Sh::new(self.script()).argv() } } pub struct Gzip; impl Cmd for Gzip { fn argv(&self) -> Argv { Argv::new("gzip") } } // busybox, which alpine based images ship, has no long options for these three pub struct Gunzip; impl Cmd for Gunzip { fn argv(&self) -> Argv { Argv::new("gunzip").arg("-c") } } pub struct Head { bytes: usize, } impl Head { pub fn bytes(bytes: usize) -> Self { Self { bytes } } } impl Cmd for Head { fn argv(&self) -> Argv { Argv::new("head").arg("-c").arg(self.bytes.to_string()) } } pub struct Rm { path: String, } impl Rm { pub fn recursive(path: &str) -> Self { Self { path: path.to_string(), } } } impl Cmd for Rm { fn argv(&self) -> Argv { Argv::new("rm").arg("-rf").arg(&self.path) } } #[cfg(test)] mod tests { use super::{Gunzip, Gzip, Head}; use crate::cmd::Cmd; #[test] fn a_pipeline_reports_a_stage_that_dies_mid_stream() { let script = Gunzip.pipe(&Gzip).unwrap().shell().unwrap(); assert_eq!(script, "sh -c 'set -o pipefail; gunzip -c | gzip'"); } #[test] fn a_pipeline_that_closes_the_pipe_on_purpose_keeps_the_default() { let script = Gunzip .pipe(&Head::bytes(512)) .unwrap() .allow_early_close() .shell() .unwrap(); assert_eq!(script, "sh -c 'gunzip -c | head -c 512'"); } #[test] fn a_pipeline_reaches_a_container_as_one_argument() { let argv = Gunzip.pipe(&Gzip).unwrap().in_container("abc123").argv(); assert_eq!( argv.words(), [ "docker", "exec", "abc123", "sh", "-c", "set -o pipefail; gunzip -c | gzip" ] ); } }