CI / build (pull_request) Successful in 12s
slim now works with any Containerfile by injecting distro-agnostic scripts at build time: - /slim/init: mounts special filesystems, configures networking (static QEMU slirp), sets up cgroup2, timezone, and rootless container prereqs. Parses slim.cmd=<base64> from the kernel cmdline for runtime overrides, otherwise execs /slim/exec. - /slim/exec: generated from the image's CMD/ENTRYPOINT (via podman image inspect), overridable via --cmd at build and run time. Changes: - New src/inject.rs: inspect image config, infer command, generate /slim/exec script, inject /slim/ into mounted rootfs - New src/scripts/slim-init.sh: the universal init script (include_str!) - build.rs: rename --init to --cmd, inject scripts before packing, drop Meta::save, restructure to ensure unmount always runs - qemu.rs: hardcode init=/slim/init, add --cmd (base64 on cmdline), drop Meta::load, add -no-reboot - Remove src/meta.rs and meta.toml (no longer needed) - Cargo.toml: add serde_json + base64, remove unused walkdir + cpio + toml - Example Containerfiles simplified to plain FROM + CMD - New example/test.sh for manual verification - README updated for new workflow
135 lines
3.9 KiB
Rust
135 lines
3.9 KiB
Rust
//! 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<Vec<String>>,
|
|
#[serde(default)]
|
|
entrypoint: Option<Vec<String>>,
|
|
#[serde(default)]
|
|
env: Option<Vec<String>>,
|
|
#[serde(default)]
|
|
working_dir: 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 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<String> = 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<String> {
|
|
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::<Vec<_>>()
|
|
.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(())
|
|
}
|