Files
boco/src/inject.rs
T
hulthe 249f2e8520
CI / build (push) Successful in 13s
Rebrand to boco
2026-09-11 17:55:53 +02:00

116 lines
3.3 KiB
Rust

//! 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<String>,
#[serde(default)]
pub entrypoint: Vec<String>,
#[serde(default)]
pub env: Vec<String>,
#[serde(default)]
pub working_dir: Option<String>,
#[serde(default)]
pub user: Option<String>,
}
pub fn inspect_config(image: &str) -> Result<Config> {
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<String> {
let parts: Vec<String> = 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<u8> {
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 `<mount>/boco/<name>` 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(())
}