Files
ahab/src/cmd/shell.rs

158 lines
3.3 KiB
Rust

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<String>) -> 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<String>,
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> {
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"
]
);
}
}