116 lines
3.3 KiB
Rust
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(())
|
|
}
|