Inject universal /slim/init instead of requiring container-specific setup
CI / build (pull_request) Successful in 12s
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
This commit is contained in:
+134
@@ -0,0 +1,134 @@
|
||||
//! 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user