//! Inject: inspect a podman image's config, infer the default command argv, //! and inject the boco init binary + null-delimited argv/env files into the //! mounted rootfs. use anyhow::{Context, Result, anyhow}; use serde::Deserialize; use std::fs; use std::path::Path; use crate::command::cmd; use crate::registry::init_binary_path; #[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 argv from the image config. /// /// Concatenates ENTRYPOINT + CMD (Docker semantics). If neither is present, /// falls back to `["/bin/sh"]`. pub fn infer_argv(config: &Config) -> Vec { let parts: Vec = config .entrypoint .iter() .chain(config.cmd.iter()) .cloned() .collect(); if parts.is_empty() { vec!["/bin/sh".into()] } else { parts } } /// Join items as null-delimited bytes. pub fn null_delimited(items: &[String]) -> Vec { let mut data = Vec::new(); for item in items { data.extend_from_slice(item.as_bytes()); data.push(0); } data } /// Install `content` into the mounted rootfs at `/boco/` with /// the given mode. fn install_into_rootfs(mount: &str, name: &str, mode: &str, content: &[u8]) -> 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}/boco/{name}"); cmd(&[ "podman", "unshare", "--", "install", "-D", "-m", mode, src, &dest, ])?; Ok(()) } /// Inject /boco/init (binary), /boco/exec (null-delimited argv), /boco/env /// (null-delimited env), and optionally /boco/user and /boco/workdir into a /// mounted container image rootfs. pub fn inject( mount_path: &Path, argv: &[String], env: &[String], user: Option<&str>, working_dir: Option<&str>, ) -> Result<()> { let mount_str = mount_path.to_str().context("mount path is not UTF-8")?; let init_binary = fs::read(init_binary_path()?).context("Failed to read boco-init binary")?; let exec_data = null_delimited(argv); let env_data = null_delimited(env); install_into_rootfs(mount_str, "init", "755", &init_binary)?; install_into_rootfs(mount_str, "exec", "644", &exec_data)?; if !env_data.is_empty() { install_into_rootfs(mount_str, "env", "644", &env_data)?; } if let Some(user) = user.filter(|u| !u.is_empty()) { install_into_rootfs(mount_str, "user", "644", user.as_bytes())?; } if let Some(dir) = working_dir.filter(|d| !d.is_empty()) { install_into_rootfs(mount_str, "workdir", "644", dir.as_bytes())?; } Ok(()) }