//! 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::fmt::Write as _; 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)] pub cmd: Vec, #[serde(default)] pub entrypoint: Vec, #[serde(default)] pub env: Vec, #[serde(default)] pub working_dir: Option, #[serde(default)] pub user: 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 parts = config.entrypoint.iter().chain(config.cmd.iter()); let parts: Vec<_> = parts.map(|s| s.as_str()).collect(); match &parts[..] { [] => "/bin/sh".into(), ["/bin/sh" | "sh", "-c", cmd] => cmd.to_string(), _ => shell_join(&parts), } } fn shell_escape(s: &str) -> String { format!("'{}'", s.replace('\'', "'\\''")) } fn shell_join(parts: &[&str]) -> 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()) { _ = writeln!(&mut lines, "cd {} 2>/dev/null", shell_escape(dir)); } for var in env { if let Some((key, val)) = var.split_once('=') { _ = writeln!( &mut lines, "export {}={}", shell_escape(key), shell_escape(val) ); } } _ = writeln!(&mut lines, "exec /bin/sh -c {}", shell_escape(command)); lines } /// Install `content` into the mounted rootfs at `/slim/` with /// the given mode. fn install_into_rootfs(mount: &str, name: &str, mode: &str, content: &str) -> Result<()> { let temp = tempfile::NamedTempFile::new()?; fs::write(temp.path(), content)?; let src = temp.path().to_str().context("temp path is not UTF-8")?; let dest = format!("{mount}/slim/{name}"); cmd(&[ "podman", "unshare", "--", "install", "-D", "-m", mode, src, &dest, ])?; Ok(()) } /// Inject /slim/init, /slim/exec, and optionally /slim/user and /slim/workdir /// into a mounted container image rootfs. pub fn inject( mount_path: &Path, exec_script: &str, user: Option<&str>, working_dir: Option<&str>, ) -> Result<()> { let mount_str = mount_path.to_str().context("mount path is not UTF-8")?; install_into_rootfs(mount_str, "init", "755", SLIM_INIT)?; install_into_rootfs(mount_str, "exec", "755", exec_script)?; if let Some(user) = user.filter(|u| !u.is_empty()) { install_into_rootfs(mount_str, "user", "644", user)?; } if let Some(dir) = working_dir.filter(|d| !d.is_empty()) { install_into_rootfs(mount_str, "workdir", "644", dir)?; } Ok(()) }