Compare commits

...
3 Commits
Author SHA1 Message Date
hulthe 0bc14cbeff Add basic README.md with an example
CI / build (push) Successful in 10s
2026-08-26 23:32:37 +02:00
hulthe 6af8660f8d Add qemu network device 2026-08-26 23:27:58 +02:00
hulthe 8b544ab688 Add image ls and image rm commands 2026-08-26 23:27:58 +02:00
7 changed files with 113 additions and 23 deletions
+16
View File
@@ -0,0 +1,16 @@
# slim
Turning containers into bootable VMs
## Example - Minimal Alpine Busybox
```sh
# Build the container
podman build ./example/alpine -t slim-alpine
# Make the container bootable by extracing initrd and vmlinuz (initial ramdisk and kernel)
slim build slim-alpine
# Boot the image using QEMU, `./example/alpine/init` will drop you into an interactive shell.
slim run slim-alpine
```
+7
View File
@@ -0,0 +1,7 @@
FROM alpine:latest
RUN mkdir -p /lib/apk/db /run
RUN apk add --no-cache --initdb linux-virt busybox util-linux
RUN cp /boot/vmlinuz-virt /vmlinuz && echo "Welcome to slim!" > /etc/motd
COPY init /init
RUN chmod +x /init
+28
View File
@@ -0,0 +1,28 @@
#!/bin/sh
# Special folders
mkdir -p /dev /proc /sys
[ -c /dev/console ] || mknod -m 600 /dev/console c 5 1
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs dev /dev
# Networking
echo slim > /proc/sys/kernel/hostname
modprobe virtio_net
ip link set lo up 2>/dev/null
ip link set eth0 up 2>/dev/null
udhcpc -i eth0 -f -q # Get DHCP lease
echo
echo 'Welcome to slim!'
echo
# Run interactive shell
/bin/sh </dev/console >/dev/console 2>&1
poweroff -f
while true; do
sleep 1
poweroff -f
done
+34
View File
@@ -0,0 +1,34 @@
use std::fs;
use crate::registry::{registry_base_dir, validate_image_name};
use anyhow::Context;
use clap::Subcommand;
#[derive(Subcommand, Debug)]
pub enum ImageCmd {
Ls,
Rm { image: String },
}
pub fn run(cmd: ImageCmd) -> anyhow::Result<()> {
match cmd {
ImageCmd::Ls => ls(),
ImageCmd::Rm { image } => rm(&image),
}
}
fn rm(image: &str) -> Result<(), anyhow::Error> {
validate_image_name(image)?;
let image_dir = registry_base_dir()?.join(image);
fs::remove_dir_all(image_dir).context("Failed to remove image dir")?;
Ok(())
}
fn ls() -> anyhow::Result<()> {
let dir = fs::read_dir(registry_base_dir()?)?;
for entry in dir {
let entry = entry?.file_name();
println!(" {}", entry.to_string_lossy());
}
Ok(())
}
+9
View File
@@ -1,10 +1,13 @@
mod build; mod build;
mod image;
mod qemu; mod qemu;
mod registry; mod registry;
use anyhow::Result; use anyhow::Result;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use crate::image::ImageCmd;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(about = "Build bootable initrd VMs from container images")] #[command(about = "Build bootable initrd VMs from container images")]
struct Cli { struct Cli {
@@ -27,6 +30,9 @@ enum Commands {
/// Image tag / registry subdir name /// Image tag / registry subdir name
name: String, name: String,
}, },
/// List slim images in the registry
#[command(subcommand)]
Image(ImageCmd),
} }
fn main() -> Result<()> { fn main() -> Result<()> {
@@ -38,6 +44,9 @@ fn main() -> Result<()> {
Commands::Run { name } => { Commands::Run { name } => {
qemu::run(&name)?; qemu::run(&name)?;
} }
Commands::Image(cmd) => {
image::run(cmd)?;
}
} }
Ok(()) Ok(())
} }
+1
View File
@@ -41,6 +41,7 @@ pub(crate) fn run(name: &str) -> Result<()> {
.arg(initrd_path.as_os_str()) .arg(initrd_path.as_os_str())
.args(["-append", &cmdline]) .args(["-append", &cmdline])
.args(["-nographic"]) .args(["-nographic"])
.args(["-nic", "user,model=virtio-net-pci"])
.status() .status()
.context("Failed to launch qemu-system-x86_64")?; .context("Failed to launch qemu-system-x86_64")?;
+18 -23
View File
@@ -1,8 +1,8 @@
//! Registry: local storage of built VM artifacts under //! Registry: local storage of built VM artifacts under
//! `$XDG_DATA_HOME/slim-rs/registry/<image>/`. //! `$XDG_DATA_HOME/slim-rs/registry/<image>/`.
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, anyhow, bail};
use std::path::PathBuf; use std::{fs, io, path::PathBuf};
/// Validate a user-supplied image name before it is used in a registry path. /// Validate a user-supplied image name before it is used in a registry path.
/// ///
@@ -26,30 +26,25 @@ pub(crate) fn validate_image_name(name: &str) -> Result<()> {
Ok(()) Ok(())
} }
pub(crate) fn registry_base_dir() -> Result<PathBuf> {
let base = xdg::BaseDirectories::with_prefix("slim-rs");
base.create_data_directory("registry")
.context("Failed to create XDG_DATA_HOME subdirectory")
}
pub(crate) fn registry_dir(image: &str) -> Result<PathBuf> { pub(crate) fn registry_dir(image: &str) -> Result<PathBuf> {
validate_image_name(image)?; validate_image_name(image)?;
let base = xdg::BaseDirectories::with_prefix("slim-rs"); let base = registry_base_dir()?;
let initrd_path = base
.place_data_file(format!("registry/{image}/initrd")) let reg_dir = base.join(image);
.context("Failed to write to XDG_DATA_HOME")?; fs::create_dir(&reg_dir)
let reg_dir = initrd_path .or_else(|e| {
.parent() (e.kind() == io::ErrorKind::AlreadyExists)
.expect("data file has a parent") .then_some(())
.to_owned(); .ok_or(e)
})
.with_context(|| anyhow!("Failed to create {reg_dir:?}"))?;
// Defense in depth: the constructed path must stay inside the registry.
let registry_root = base
.place_data_file("registry/.root-check")
.context("Failed to write to XDG_DATA_HOME")?
.parent()
.expect("data file has a parent")
.to_owned();
if !reg_dir.starts_with(&registry_root) {
bail!(
"invalid image name '{image}': registry path escapes {}",
registry_root.display()
);
}
Ok(reg_dir) Ok(reg_dir)
} }