use crate::errors::*;
use serde::{Deserialize, Serialize};
use std::ffi::OsStr;
use std::fmt;
use std::io::Read;
use std::process::Stdio;
use std::str::FromStr;
use tokio::process::Command;
#[derive(Debug, PartialEq, Clone)]
pub struct ImageRef {
pub repo: String,
pub tag: Option<String>,
pub digest: Option<String>,
}
impl FromStr for ImageRef {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
if let Some((repo, digest)) = s.split_once('@') {
Ok(ImageRef {
repo: repo.to_string(),
tag: None,
digest: Some(digest.to_string()),
})
} else if let Some((repo, tag)) = s.split_once(':') {
Ok(ImageRef {
repo: repo.to_string(),
tag: Some(tag.to_string()),
digest: None,
})
} else {
Ok(ImageRef {
repo: s.to_string(),
tag: None,
digest: None,
})
}
}
}
impl ToString for ImageRef {
fn to_string(&self) -> String {
let repo = &self.repo;
if let Some(digest) = &self.digest {
format!("{repo}@{digest}")
} else if let Some(tag) = &self.tag {
format!("{repo}:{tag}")
} else {
repo.to_string()
}
}
}
pub async fn podman<I, S>(args: I, capture_stdout: bool) -> Result<Vec<u8>>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr> + fmt::Debug,
{
let mut cmd = Command::new("podman");
let args = args.into_iter().collect::<Vec<_>>();
cmd.args(&args);
if capture_stdout {
cmd.stdout(Stdio::piped());
}
debug!("Spawning child process: podman {:?}", args);
let child = cmd.spawn().context("Failed to execute podman binary")?;
let out = child.wait_with_output().await?;
debug!("Podman command exited: {:?}", out.status);
if !out.status.success() {
bail!(
"Podman command ({:?}) failed to execute: {:?}",
args,
out.status
);
}
Ok(out.stdout)
}
pub async fn pull(image: &str) -> Result<()> {
podman(&["image", "pull", "--", image], false).await?;
Ok(())
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Image {
pub digest: String,
}
pub async fn inspect(image: &str) -> Result<Vec<Image>> {
let inspect = podman(&["image", "inspect", "--", image], true).await?;
let inspect = serde_json::from_slice::<Vec<Image>>(&inspect)?;
debug!("Image inspect result: {inspect:?}");
Ok(inspect)
}
#[derive(Debug)]
pub struct Config<'a> {
pub init: &'a [String],
pub mounts: &'a [(String, String)],
pub expose_fuse: bool,
}
#[derive(Debug, Default)]
pub struct Exec<'a> {
pub capture_stdout: bool,
pub cwd: Option<&'a str>,
pub user: Option<&'a str>,
}
#[derive(Debug)]
pub struct Container {
pub id: String,
}
impl Container {
pub async fn create(image: &str, config: Config<'_>) -> Result<Container> {
let bin = config
.init
.first()
.context("Command for container can't be empty")?;
let cmd_args = &config.init[1..];
let entrypoint = format!("--entrypoint={bin}");
let mut podman_args = vec![
"container".to_string(),
"run".to_string(),
"--detach".to_string(),
"--rm".to_string(),
"--network=host".to_string(),
"-v=/usr/bin/catatonit:/__:ro".to_string(),
];
for (src, dest) in config.mounts {
podman_args.push(format!("-v={src}:{dest}"));
}
if config.expose_fuse {
debug!("Mapping /dev/fuse into the container");
podman_args.push("--device=/dev/fuse".to_string());
}
podman_args.extend([entrypoint, "--".to_string(), image.to_string()]);
podman_args.extend(cmd_args.iter().map(|s| s.to_string()));
let mut out = podman(&podman_args, true).await?;
if let Some(idx) = memchr::memchr(b'\n', &out) {
out.truncate(idx);
}
let id = String::from_utf8(out)?;
Ok(Container { id })
}
pub async fn exec<I, S>(&self, args: I, options: Exec<'_>) -> Result<Vec<u8>>
where
I: IntoIterator<Item = S>,
S: AsRef<str> + fmt::Debug + Clone,
{
let args = args.into_iter().collect::<Vec<_>>();
let mut a = vec!["container".to_string(), "exec".to_string()];
if let Some(cwd) = options.cwd {
a.extend(["-w".to_string(), cwd.to_string()]);
}
if let Some(user) = options.user {
a.extend(["-u".to_string(), user.to_string()]);
}
a.extend(["--".to_string(), self.id.to_string()]);
a.extend(args.iter().map(|x| x.as_ref().to_string()));
let buf = podman(&a, options.capture_stdout)
.await
.with_context(|| anyhow!("Failed to execute in container: {:?}", args))?;
Ok(buf)
}
pub async fn cat(&self, path: &str) -> Result<Vec<u8>> {
let a = vec![
"container".to_string(),
"cp".to_string(),
"--".to_string(),
format!("{}:{}", self.id, path),
"-".to_string(),
];
let buf = podman(&a, true)
.await
.with_context(|| anyhow!("Failed to read from container: {:?}", path))?;
let mut tar = tar::Archive::new(&buf[..]);
let mut entries = tar.entries()?;
let entry = entries
.next()
.context("Tar archive generated by podman cp is empty")?;
let mut entry = entry?;
let mut buf = Vec::new();
entry.read_to_end(&mut buf)?;
Ok(buf)
}
pub async fn kill(self) -> Result<()> {
podman(&["container", "kill", &self.id], true)
.await
.context("Failed to remove container")?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_image_ref() -> Result<()> {
let image_ref = ImageRef::from_str("rust")?;
assert_eq!(
image_ref,
ImageRef {
repo: "rust".to_string(),
tag: None,
digest: None,
}
);
Ok(())
}
#[test]
fn test_parse_image_ref_digest() -> Result<()> {
let image_ref = ImageRef::from_str(
"rust@sha256:28ee8822965a932e229599b59928f8c2655b2a198af30568acf63e8aff0e8a3a",
)?;
assert_eq!(
image_ref,
ImageRef {
repo: "rust".to_string(),
tag: None,
digest: Some(
"sha256:28ee8822965a932e229599b59928f8c2655b2a198af30568acf63e8aff0e8a3a"
.to_string()
),
}
);
Ok(())
}
#[test]
fn test_parse_image_ref_tag() -> Result<()> {
let image_ref = ImageRef::from_str("rust:1-alpine3.18")?;
assert_eq!(
image_ref,
ImageRef {
repo: "rust".to_string(),
tag: Some("1-alpine3.18".to_string()),
digest: None,
}
);
Ok(())
}
}