automod::dir!(pub(crate) "src/podman");
use std::{
ffi::OsStr,
path::PathBuf,
pin::Pin,
process::{self, Stdio},
};
use anyhow::{anyhow, bail, Context, Error, Result};
use futures::{
stream::{self, select},
Stream, StreamExt, TryStreamExt,
};
use once_cell::sync::Lazy;
use tokio::{
io::{AsyncBufReadExt, BufReader},
process::Command,
};
use tokio_stream::wrappers::LinesStream;
use self::types::Version;
use crate::config::Config;
static PODMAN_MIN_SUPPORTED_VERSION: Lazy<semver::Version> =
Lazy::new(|| semver::Version::new(4, 3, 0));
pub(crate) struct Podman {
project_directory: PathBuf,
dry_run: bool,
}
impl Podman {
pub(crate) async fn new(config: &Config) -> Result<Self> {
let podman = Self {
project_directory: config.project_directory.clone(),
dry_run: config.dry_run,
};
let output = podman.force_run(["version", "--format", "json"]).await?;
let version = serde_json::from_str::<Version>(&output)
.with_context(|| anyhow!("Podman version not recognised"))?
.client
.version;
if version < *PODMAN_MIN_SUPPORTED_VERSION {
bail!(
"Only Podman {} and above is supported: version {version} found",
*PODMAN_MIN_SUPPORTED_VERSION
);
}
Ok(podman)
}
fn command<I, S>(&self, args: I) -> Command
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let mut command = Command::new("podman");
command.current_dir(&self.project_directory).args(args);
command
}
pub(crate) async fn run<I, S>(&self, args: I) -> Result<String>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
if self.dry_run {
println!(
"`podman {}`",
shell_words::join(
args.into_iter()
.map(|arg| arg.as_ref().to_string_lossy().to_string())
)
);
Ok(String::new())
} else {
self.force_run(args).await
}
}
pub(crate) async fn force_run<I, S>(&self, args: I) -> Result<String>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let mut command = self.command(args);
let output = command.output().await.with_context(|| {
anyhow!(
"`{} {}` cannot be executed",
command.as_std().get_program().to_string_lossy(),
shell_words::join(
command
.as_std()
.get_args()
.map(|arg| arg.to_string_lossy().to_string())
)
)
})?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
} else {
Err(
anyhow!("{}", String::from_utf8_lossy(&output.stderr)).context(anyhow!(
"`{} {}` returned an error",
command.as_std().get_program().to_string_lossy(),
shell_words::join(
command
.as_std()
.get_args()
.map(|arg| arg.to_string_lossy().to_string())
)
)),
)
}
}
pub(crate) fn watch<I, S>(&self, args: I) -> Result<Pin<Box<dyn Stream<Item = Result<String>>>>>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
if self.dry_run {
println!(
"`podman {}`",
shell_words::join(
args.into_iter()
.map(|arg| arg.as_ref().to_string_lossy().to_string())
)
);
Ok(stream::empty().boxed())
} else {
let child = self
.command(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let stdout = BufReader::new(child.stdout.unwrap()).lines();
let stderr = BufReader::new(child.stderr.unwrap()).lines();
Ok(select(LinesStream::new(stdout), LinesStream::new(stderr))
.map_err(Error::from)
.boxed())
}
}
pub(crate) async fn attach<I, S>(&self, args: I) -> Result<()>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
if self.dry_run {
println!(
"`podman {}`",
shell_words::join(
args.into_iter()
.map(|arg| arg.as_ref().to_string_lossy().to_string())
)
);
Ok(())
} else {
let status = self.command(args).spawn()?.wait().await?;
if !status.success() {
process::exit(status.code().unwrap_or(1));
}
Ok(())
}
}
}