//! Inject: inspect a podman image's config, infer the default command, and //! inject distro-agnostic /slim/ scripts (init + exec) into the mounted rootfs. use anyhow::{Context, Result, anyhow}; use serde::Deserialize; use std::fs; use std::path::Path; use crate::command::cmd; const SLIM_INIT: &str = include_str!("scripts/slim-init.sh"); #[derive(Debug, Default, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct Config { #[serde(default)] cmd: Option>, #[serde(default)] entrypoint: Option>, #[serde(default)] env: Option>, #[serde(default)] working_dir: Option, } pub fn inspect_config(image: &str) -> Result { let json = cmd(&[ "podman", "image", "inspect", image, "--format", "{{json .Config}}", ])?; let config: Config = serde_json::from_str(json.trim()) .with_context(|| anyhow!("failed to parse image inspect output for '{image}'"))?; Ok(config) } /// Infer the command string from the image config. /// /// Concatenates ENTRYPOINT + CMD (Docker semantics). If neither is present, /// falls back to `/bin/sh`. pub fn infer_command(config: &Config) -> String { let entrypoint = config.entrypoint.as_deref().filter(|e| !e.is_empty()); let cmd = config.cmd.as_deref().filter(|c| !c.is_empty()); let parts: Vec = match (entrypoint, cmd) { (Some(ep), Some(c)) => { let mut parts = ep.to_vec(); parts.extend(c.iter().cloned()); parts } (Some(ep), None) => ep.to_vec(), (None, Some(c)) => c.to_vec(), (None, None) => vec!["/bin/sh".to_string()], }; if parts.len() == 3 && (parts[0] == "/bin/sh" || parts[0] == "sh") && parts[1] == "-c" { parts[2].clone() } else { shell_join(&parts) } } pub fn env(config: &Config) -> Vec { config.env.clone().unwrap_or_default() } pub fn working_dir(config: &Config) -> Option<&str> { config.working_dir.as_deref() } fn shell_escape(s: &str) -> String { format!("'{}'", s.replace('\'', "'\\''")) } fn shell_join(parts: &[String]) -> String { parts .iter() .map(|p| shell_escape(p)) .collect::>() .join(" ") } /// Generate the /slim/exec script from a command string, env vars, and working dir. pub fn build_exec_script(command: &str, env: &[String], working_dir: Option<&str>) -> String { let mut lines = String::from("#!/bin/sh\n"); if let Some(dir) = working_dir.filter(|d| !d.is_empty()) { lines.push_str(&format!("cd {} 2>/dev/null\n", shell_escape(dir))); } for var in env { if let Some((key, val)) = var.split_once('=') { lines.push_str(&format!( "export {}={}\n", shell_escape(key), shell_escape(val) )); } } lines.push_str(&format!("exec /bin/sh -c {}\n", shell_escape(command))); lines } /// Inject /slim/init and /slim/exec into a mounted container image rootfs. pub fn inject(mount_path: &Path, exec_script: &str) -> Result<()> { let mount_str = mount_path.to_str().context("mount path is not UTF-8")?; let init_temp = tempfile::NamedTempFile::new()?; let exec_temp = tempfile::NamedTempFile::new()?; fs::write(init_temp.path(), SLIM_INIT)?; fs::write(exec_temp.path(), exec_script)?; let init_src = init_temp .path() .to_str() .context("temp path is not UTF-8")?; let exec_src = exec_temp .path() .to_str() .context("temp path is not UTF-8")?; let init_dest = format!("{mount_str}/slim/init"); let exec_dest = format!("{mount_str}/slim/exec"); cmd(&[ "podman", "unshare", "--", "install", "-D", "-m", "755", init_src, &init_dest, ])?; cmd(&[ "podman", "unshare", "--", "install", "-D", "-m", "755", exec_src, &exec_dest, ])?; Ok(()) }