diff --git a/docs/1-installing.md b/docs/1-installing.md index b1c2822..44cb689 100644 --- a/docs/1-installing.md +++ b/docs/1-installing.md @@ -35,7 +35,7 @@ To also set up crun-vm for use with Docker: 1. Install crun-vm's runtime dependencies: ```console - $ dnf install bash coreutils crun genisoimage grep libselinux-devel libvirt-client libvirt-daemon-driver-qemu libvirt-daemon-log openssh-clients qemu-img qemu-system-x86-core shadow-utils util-linux virtiofsd + $ dnf install bash coreutils crun crun-krun genisoimage grep libselinux-devel libvirt-client libvirt-daemon-driver-qemu libvirt-daemon-log openssh-clients qemu-img qemu-system-x86-core sed shadow-utils util-linux virtiofsd ``` 2. Install Rust and Cargo if you do not already have Rust tooling available: diff --git a/docs/2-podman-docker.md b/docs/2-podman-docker.md index 47be99c..a7d9926 100644 --- a/docs/2-podman-docker.md +++ b/docs/2-podman-docker.md @@ -96,6 +96,25 @@ in a container image. Note that flag `--persistent` has no effect when running VMs from container images. +### From bootable container images + +crun-vm can also work with [bootable container images], which are containers +that package a full operating system: + +```console +$ podman run \ + --runtime crun-vm \ + -it --rm \ + quay.io/fedora/fedora-bootc:40 +``` + +Internally, crun-vm generates a VM image from the bootable container and then +boots it. + +By default, the VM image is given a disk size roughly double the size of the +bootc container image. To change this, use the `--bootc-disk-size <size>[KMGT]` +option. + ## First-boot customization ### cloud-init @@ -317,8 +336,12 @@ $ podman run \ ### System emulation To use system emulation instead of hardware-assisted virtualization, specify the -`--emulated` flag. Without this flag, attempting to create a VM on a host tbat -doesn't support KVM will fail. +`--emulated` flag. Without this flag, attempting to create a VM from a guest +with a different architecture from the host's or on a host that doesn't support +KVM will fail. + +It's not currently possible to use this flag when the container image is a bootc +bootable container. ### Inspecting and customizing the libvirt domain XML @@ -340,6 +363,7 @@ be merged with it using the non-standard option `--merge-libvirt-xml <file>`. > Before using this flag, consider if you would be better served using libvirt > directly to manage your VM. +[bootable container images]: https://containers.github.io/bootable/ [cloud-init]: https://cloud-init.io/ [domain XML definition]: https://libvirt.org/formatdomain.html [Ignition]: https://coreos.github.io/ignition/ diff --git a/embed/bootc/config.json b/embed/bootc/config.json new file mode 100644 index 0000000..a40fc6c --- /dev/null +++ b/embed/bootc/config.json @@ -0,0 +1,88 @@ +{ + "ociVersion": "1.0.0", + "process": { + "terminal": true, + "user": { "uid": 0, "gid": 0 }, + "args": ["/output/entrypoint.sh", "<IMAGE_NAME>"], + "env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "TERM=xterm" + ], + "cwd": "/", + "capabilities": { + "bounding": [], + "effective": [], + "inheritable": [], + "permitted": [], + "ambient": [] + }, + "rlimits": [ + { + "type": "RLIMIT_NOFILE", + "hard": 262144, + "soft": 262144 + } + ], + "noNewPrivileges": true + }, + "root": { + "path": "<ORIGINAL_ROOT>", + "readonly": false + }, + "hostname": "bootc-install", + "mounts": [ + { + "type": "bind", + "source": "<PRIV_DIR>/root/crun-vm/bootc", + "destination": "/output", + "options": ["bind", "rprivate", "rw"] + }, + { + "destination": "/proc", + "type": "proc", + "source": "proc" + }, + { + "destination": "/dev/pts", + "type": "devpts", + "source": "devpts", + "options": [ + "nosuid", + "noexec", + "newinstance", + "ptmxmode=0666", + "mode=0620", + "gid=5" + ] + } + ], + "linux": { + "namespaces": [ + { "type": "pid" }, + { "type": "network" }, + { "type": "ipc" }, + { "type": "uts" }, + { "type": "cgroup" }, + { "type": "mount" } + ], + "maskedPaths": [ + "/proc/acpi", + "/proc/asound", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/sys/firmware", + "/proc/scsi" + ], + "readonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + } +} diff --git a/embed/bootc/entrypoint.sh b/embed/bootc/entrypoint.sh new file mode 100644 index 0000000..3798c5c --- /dev/null +++ b/embed/bootc/entrypoint.sh @@ -0,0 +1,51 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later + +set -e + +image_name=$1 + +# monkey-patch loopdev partition detection, given we're not running systemd +# (bootc runs `udevadm settle` as a way to wait until loopdev partitions are +# detected; we hijack that call and use partx to set up the partition devices) + +original_udevadm=$( which udevadm ) + +mkdir -p /output/bin + +cat >/output/bin/udevadm <<EOF +#!/bin/sh +${original_udevadm@Q} "\$@" && partx --add /dev/loop0 +EOF + +chmod +x /output/bin/udevadm + +# default to an xfs root file system if there is no bootc config (some images +# don't currently provide any, for instance quay.io/fedora/fedora-bootc:40) + +if ! find /usr/lib/bootc/install -mindepth 1 -maxdepth 1 | read; then + # /usr/lib/bootc/install is empty + + cat >/usr/lib/bootc/install/00-crun-vm.toml <<EOF + [install.filesystem.root] + type = "xfs" +EOF + +fi + +# build disk image using bootc-install + +PATH=/output/bin:$PATH bootc install to-disk \ + --source-imgref docker-archive:/output/image.docker-archive \ + --target-imgref "$image_name" \ + --skip-fetch-check \ + --generic-image \ + --via-loopback \ + --karg console=tty0 \ + --karg console=ttyS0 \ + --karg selinux=0 \ + /output/image.raw + +# communicate success by creating a file, since krun always exits successfully + +touch /output/bootc-install-success diff --git a/embed/bootc/prepare.sh b/embed/bootc/prepare.sh new file mode 100644 index 0000000..7b90905 --- /dev/null +++ b/embed/bootc/prepare.sh @@ -0,0 +1,127 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0-or-later + +set -o errexit -o pipefail -o nounset + +engine=$1 +container_id=$2 +original_root=$3 +priv_dir=$4 +disk_size=$5 + +__step() { + printf "\033[36m%s\033[0m\n" "$*" +} + +bootc_dir=$priv_dir/root/crun-vm/bootc + +mkfifo "$bootc_dir/progress" +exec > "$bootc_dir/progress" 2>&1 + +# this blocks here until the named pipe above is opened by entrypoint.sh + +# get info about the container *image* + +image_info=$( + "$engine" container inspect \ + --format '{{.Config.Image}}'$'\t''{{.Image}}' \ + "$container_id" + ) + +image_name=$( cut -f1 <<< "$image_info" ) +# image_name=${image_name#sha256:} + +image_id=$( cut -f2 <<< "$image_info" ) + +# check if VM image is cached + +container_name=crun-vm-$container_id + +cache_image_label=containers.crun-vm.from=$image_id +cache_image_id=$( "$engine" images --filter "label=$cache_image_label" --format '{{.Id}}' ) + +if [[ -n "$cache_image_id" ]]; then + + # retrieve VM image from cached containerdisk + + __step "Retrieving cached VM image..." + + trap '"$engine" rm --force "$container_name" >/dev/null 2>&1 || true' EXIT + + "$engine" create --quiet --name "$container_name" "$cache_image_id" >/dev/null + "$engine" export "$container_name" | tar -C "$bootc_dir" -x image.qcow2 + "$engine" rm "$container_name" >/dev/null 2>&1 + + trap '' EXIT + +else + + __step "Converting $image_name into a VM image..." + + # save container *image* as an archive + + echo -n 'Preparing container image...' + + "$engine" save --output "$bootc_dir/image.docker-archive" "$image_id" 2>&1 \ + | sed -u 's/.*/./' \ + | stdbuf -o0 tr -d '\n' + + echo + + # adjust krun config + + __sed() { + sed -i "s|$1|$2|" "$bootc_dir/config.json" + } + + __sed "<IMAGE_NAME>" "$image_name" + __sed "<ORIGINAL_ROOT>" "$original_root" + __sed "<PRIV_DIR>" "$priv_dir" + + # run bootc-install under krun + + if [[ -z "$disk_size" ]]; then + container_image_size=$( + "$engine" image inspect --format '{{.VirtualSize}}' "$image_id" + ) + + # use double the container image size to allow for in-place updates + disk_size=$(( container_image_size * 2 )) + + # round up to 1 MiB + alignment=$(( 2**20 )) + disk_size=$(( (disk_size + alignment - 1) / alignment * alignment )) + fi + + truncate --size "$disk_size" "$bootc_dir/image.raw" + + trap 'krun delete --force "$container_name" >/dev/null 2>&1 || true' EXIT + krun run --config "$bootc_dir/config.json" "$container_name" </dev/ptmx + trap '' EXIT + + [[ -e "$bootc_dir/bootc-install-success" ]] + + # convert image to qcow2 to get a lower file length + + qemu-img convert -f raw -O qcow2 "$bootc_dir/image.raw" "$bootc_dir/image.qcow2" + rm "$bootc_dir/image.raw" + + # cache VM image file as containerdisk + + __step "Caching VM image as a containerdisk..." + + id=$( + "$engine" build --quiet --file - --label "$cache_image_label" "$bootc_dir" <<-'EOF' + FROM scratch + COPY image.qcow2 / + ENTRYPOINT ["no-entrypoint"] +EOF + ) + + echo "Stored as untagged container image with ID $id" + +fi + +__step "Booting VM..." + +touch "$bootc_dir/success" diff --git a/scripts/entrypoint.sh b/embed/entrypoint.sh similarity index 87% rename from scripts/entrypoint.sh rename to embed/entrypoint.sh index c0fffc0..18fa71e 100644 --- a/scripts/entrypoint.sh +++ b/embed/entrypoint.sh @@ -5,6 +5,8 @@ trap 'exit 143' SIGTERM set -o errexit -o pipefail -o nounset +is_bootc_container=$1 + # clean up locks that may have been left around from the container being killed rm -fr /var/lock @@ -53,6 +55,22 @@ virsh --connect "qemu+unix:///session?socket=$socket" "\$@" EOF chmod +x /crun-vm/virsh +# wait until VM image is generated from bootable container (if applicable) + +if (( is_bootc_container == 1 )) && [[ ! -e /crun-vm/image/image ]]; then + + fifo=/crun-vm/bootc/progress + while [[ ! -e "$fifo" ]]; do sleep 0.2; done + cat "$fifo" + rm "$fifo" + + [[ -e /crun-vm/bootc/success ]] + + mkdir -p /crun-vm/image + mv /crun-vm/bootc/image.qcow2 /crun-vm/image/image + +fi + # launch VM function __bg_ensure_tty() { diff --git a/scripts/exec.sh b/embed/exec.sh similarity index 100% rename from scripts/exec.sh rename to embed/exec.sh diff --git a/scripts/virtiofsd.sh b/embed/virtiofsd.sh similarity index 100% rename from scripts/virtiofsd.sh rename to embed/virtiofsd.sh diff --git a/plans/tests.fmf b/plans/tests.fmf index c98e0fd..42db367 100644 --- a/plans/tests.fmf +++ b/plans/tests.fmf @@ -11,6 +11,7 @@ prepare: - cargo - coreutils - crun + - crun-krun - docker - genisoimage - grep diff --git a/src/commands/create/custom_opts.rs b/src/commands/create/custom_opts.rs index b62b820..d02a063 100644 --- a/src/commands/create/custom_opts.rs +++ b/src/commands/create/custom_opts.rs @@ -9,7 +9,7 @@ use clap::Parser; use lazy_static::lazy_static; use regex::Regex; -use crate::commands::create::runtime_env::RuntimeEnv; +use crate::commands::create::engine::Engine; #[derive(Clone, Debug)] pub struct Blockdev { @@ -55,6 +55,9 @@ pub struct CustomOptions { #[clap(long, help = "Use system emulation rather than KVM")] pub emulated: bool, + #[clap(long)] + pub bootc_disk_size: Option<String>, + #[clap(long)] pub cloud_init: Option<Utf8PathBuf>, @@ -75,7 +78,7 @@ pub struct CustomOptions { } impl CustomOptions { - pub fn from_spec(spec: &oci_spec::runtime::Spec, env: RuntimeEnv) -> Result<Self> { + pub fn from_spec(spec: &oci_spec::runtime::Spec, engine: Engine) -> Result<Self> { let mut args: Vec<&String> = spec .process() .as_ref() @@ -87,7 +90,14 @@ impl CustomOptions { .collect(); if let Some(&first_arg) = args.first() { - if first_arg == "no-entrypoint" { + let ignore = [ + "no-entrypoint", + "/sbin/init", + "/usr/sbin/init", + "/usr/local/sbin/init", + ]; + + if ignore.contains(&first_arg.as_str()) { args.remove(0); } } @@ -151,7 +161,7 @@ impl CustomOptions { ), ); - if env == RuntimeEnv::Kubernetes { + if engine == Engine::Kubernetes { for blockdev in &mut options.blockdev { blockdev.source = path_in_container_into_path_in_host(spec, &blockdev.source)?; blockdev.target = path_in_container_into_path_in_host(spec, &blockdev.target)?; diff --git a/src/commands/create/domain.rs b/src/commands/create/domain.rs index 13964e3..f6d6bfd 100644 --- a/src/commands/create/domain.rs +++ b/src/commands/create/domain.rs @@ -58,11 +58,13 @@ fn generate( st(w, "memory", &[("unit", "b")], memory.as_str())?; s(w, "os", &[("firmware", "efi")], |w| { - let attrs = match ["x86", "x86_64"].contains(&env::consts::ARCH) { - true => [("machine", "q35")].as_slice(), - false => [].as_slice(), // use libvirt's default - }; - st(w, "type", attrs, "hvm")?; + let guest_arch = vm_image_info.arch.as_deref().unwrap_or(env::consts::ARCH); + + let mut attrs = vec![("arch", guest_arch)]; + if ["x86", "x86_64"].contains(&guest_arch) { + attrs.push(("machine", "q35")); + } + st(w, "type", &attrs, "hvm")?; s(w, "firmware", &[], |w| { se(w, "feature", &[("enabled", "no"), ("name", "secure-boot")]) diff --git a/src/commands/create/engine.rs b/src/commands/create/engine.rs new file mode 100644 index 0000000..a989d53 --- /dev/null +++ b/src/commands/create/engine.rs @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +use std::fs; +use std::path::Path; + +use anyhow::{bail, Result}; +use camino::Utf8Path; +use lazy_static::lazy_static; +use regex::Regex; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Engine { + Podman, + Docker, + Kubernetes, +} + +impl Engine { + pub fn command(self) -> Option<&'static str> { + match self { + Engine::Podman => Some("podman"), + Engine::Docker => Some("docker"), + Engine::Kubernetes => None, + } + } + + pub fn detect( + container_id: &str, + bundle_path: &Utf8Path, + spec: &oci_spec::runtime::Spec, + original_root_path: impl AsRef<Utf8Path>, + ) -> Result<Engine> { + // TODO: Make this absolutely robust and secure. Probably require engine config to pass us + // an option specifying what engine is running crun-vm. + + // check if we're under CRI-O under Kubernetes + + { + let has_kubernetes_secrets_dir = spec.mounts().iter().flatten().any(|m| { + m.destination() + .starts_with("/var/run/secrets/kubernetes.io") + }); + + let has_kubernetes_managed_etc_hosts = spec + .mounts() + .iter() + .flatten() + .filter(|m| m.destination() == Utf8Path::new("/etc/hosts")) + .flat_map(|m| m.source()) + .next() + .map(fs::read_to_string) + .transpose()? + .and_then(|hosts| hosts.lines().next().map(|line| line.to_string())) + .map(|line| line.contains("Kubernetes-managed hosts file")) + .unwrap_or(false); + + if has_kubernetes_secrets_dir || has_kubernetes_managed_etc_hosts { + return Ok(Engine::Kubernetes); + } + } + + // check if we're under Docker + + { + let has_dot_dockerenv_file = original_root_path + .as_ref() + .join(".dockerenv") + .try_exists()?; + + if has_dot_dockerenv_file { + return Ok(Engine::Docker); + } + } + + // check if we're under Podman + + { + let has_mount_on = |p| { + spec.mounts() + .iter() + .flatten() + .any(|m| m.destination() == Path::new(p)) + }; + + let has_dot_containerenv_file = + has_mount_on("/run/.containerenv") || has_mount_on("/var/run/.containerenv"); + + lazy_static! { + static ref BUNDLE_PATH_PATTERN: Regex = + Regex::new(r"/overlay-containers/([^/]+)/userdata$").unwrap(); + } + + let is_podman_bundle_path = match BUNDLE_PATH_PATTERN.captures(bundle_path.as_str()) { + Some(captures) => &captures[1] == container_id, + None => false, + }; + + if has_dot_containerenv_file && is_podman_bundle_path { + return Ok(Engine::Podman); + } + } + + // unknown engine + + bail!("could not identify container engine; crun-vm current only supports Podman, Docker, and Kubernetes"); + } +} diff --git a/src/commands/create/mod.rs b/src/commands/create/mod.rs index dcc557d..2370a03 100644 --- a/src/commands/create/mod.rs +++ b/src/commands/create/mod.rs @@ -2,28 +2,31 @@ mod custom_opts; mod domain; +mod engine; mod first_boot; -mod runtime_env; use std::ffi::OsStr; use std::fs::{self, File, Permissions}; use std::io::ErrorKind; use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; use std::path::Path; -use std::process::Command; +use std::process::{Command, Stdio}; use anyhow::{anyhow, bail, ensure, Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; +use lazy_static::lazy_static; use nix::sys::stat::{major, makedev, minor, mknod, Mode, SFlag}; +use regex::Regex; use rust_embed::RustEmbed; use crate::commands::create::custom_opts::CustomOptions; use crate::commands::create::domain::set_up_libvirt_domain_xml; +use crate::commands::create::engine::Engine; use crate::commands::create::first_boot::FirstBootConfig; -use crate::commands::create::runtime_env::RuntimeEnv; use crate::util::{ bind_mount_dir_with_different_context, bind_mount_file, create_overlay_vm_image, crun, - find_single_file_in_dirs, is_mountpoint, set_file_context, SpecExt, VmImageInfo, + find_single_file_in_dirs, fix_selinux_label, is_mountpoint, set_file_context, SpecExt, + VmImageInfo, }; pub fn create(args: &liboci_cli::Create, raw_args: &[impl AsRef<OsStr>]) -> Result<()> { @@ -31,28 +34,13 @@ pub fn create(args: &liboci_cli::Create, raw_args: &[impl AsRef<OsStr>]) -> Resu let config_path = bundle_path.join("config.json"); let mut spec = oci_spec::runtime::Spec::load(&config_path)?; - let original_root_path: Utf8PathBuf = spec.root_path()?.canonicalize()?.try_into()?; // ensure absolute - - if let Some(process) = spec.process().as_ref() { - if let Some(capabilities) = process.capabilities().as_ref() { - fn any_is_cap_sys_admin(caps: &Option<oci_spec::runtime::Capabilities>) -> bool { - caps.as_ref() - .is_some_and(|set| set.contains(&oci_spec::runtime::Capability::SysAdmin)) - } + ensure_unprivileged(&spec)?; - ensure!( - !any_is_cap_sys_admin(capabilities.bounding()) - && !any_is_cap_sys_admin(capabilities.effective()) - && !any_is_cap_sys_admin(capabilities.inheritable()) - && !any_is_cap_sys_admin(capabilities.permitted()) - && !any_is_cap_sys_admin(capabilities.ambient()), - "crun-vm is incompatible with privileged containers" - ); - } - } + let original_root_path: Utf8PathBuf = spec.root_path()?.canonicalize()?.try_into()?; // ensure absolute - let runtime_env = RuntimeEnv::current(&spec, &original_root_path)?; - let custom_options = CustomOptions::from_spec(&spec, runtime_env)?; + let engine = Engine::detect(&args.container_id, bundle_path, &spec, &original_root_path)?; + let custom_options = CustomOptions::from_spec(&spec, engine)?; + let is_bootc_container = is_bootc_container(&original_root_path, &custom_options, engine)?; // We include container_id in our paths to ensure no overlap with the user container's contents. let priv_dir_path = original_root_path.join(format!("crun-vm-{}", args.container_id)); @@ -65,7 +53,13 @@ pub fn create(args: &liboci_cli::Create, raw_args: &[impl AsRef<OsStr>]) -> Resu set_file_context(&priv_dir_path, context)?; } - set_up_container_root(&mut spec, &priv_dir_path, &custom_options)?; + set_up_container_root( + &mut spec, + &priv_dir_path, + &custom_options, + is_bootc_container, + )?; + let is_first_create = is_first_create(&spec)?; let base_vm_image_info = set_up_vm_image( @@ -74,6 +68,7 @@ pub fn create(args: &liboci_cli::Create, raw_args: &[impl AsRef<OsStr>]) -> Resu &priv_dir_path, &custom_options, is_first_create, + is_bootc_container, )?; let mut mounts = Mounts::default(); @@ -87,7 +82,7 @@ pub fn create(args: &liboci_cli::Create, raw_args: &[impl AsRef<OsStr>]) -> Resu let ssh_pub_key = set_up_ssh_key_pair( &mut spec, &custom_options, - runtime_env, + engine, &priv_dir_path, is_first_create, )?; @@ -104,9 +99,97 @@ pub fn create(args: &liboci_cli::Create, raw_args: &[impl AsRef<OsStr>]) -> Resu crun(raw_args)?; // actually create container + if is_first_create && is_bootc_container { + // We want to ask podman what our image name is, so we can give it to bootc-install, but we + // can't wait synchronously for a response since podman hangs until this create command + // completes. We then want to run bootc-install under krun, which already isolates the + // workload and so can be run outside of our container. We thus launch a process that + // asynchronously performs these steps, and share its progress and output with out + // container's entrypoint through a named pipe. + // + // Note that this process blocks until our container's entrypoint actually starts running, + // thus after the "start" OCI runtime command is called. + + let bootc_dir = priv_dir_path.join("root/crun-vm/bootc"); + fs::create_dir_all(&bootc_dir)?; + + std::process::Command::new(bootc_dir.join("prepare.sh")) + .arg(engine.command().unwrap()) + .arg(&args.container_id) + .arg(&original_root_path) + .arg(&priv_dir_path) + .arg(custom_options.bootc_disk_size.unwrap_or_default()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + } + Ok(()) } +fn ensure_unprivileged(spec: &oci_spec::runtime::Spec) -> Result<()> { + if let Some(process) = spec.process().as_ref() { + if let Some(capabilities) = process.capabilities().as_ref() { + fn any_is_cap_sys_admin(caps: &Option<oci_spec::runtime::Capabilities>) -> bool { + caps.as_ref() + .is_some_and(|set| set.contains(&oci_spec::runtime::Capability::SysAdmin)) + } + + ensure!( + !any_is_cap_sys_admin(capabilities.bounding()) + && !any_is_cap_sys_admin(capabilities.effective()) + && !any_is_cap_sys_admin(capabilities.inheritable()) + && !any_is_cap_sys_admin(capabilities.permitted()) + && !any_is_cap_sys_admin(capabilities.ambient()), + "crun-vm is incompatible with privileged containers" + ); + } + } + + Ok(()) +} + +fn is_bootc_container( + original_root_path: &Utf8Path, + custom_options: &CustomOptions, + engine: Engine, +) -> Result<bool> { + let is_bootc_container = original_root_path.join("usr/lib/bootc/install").is_dir(); + + ensure!( + !is_bootc_container || engine == Engine::Podman || engine == Engine::Docker, + "bootc containers are only supported with Podman and Docker" + ); + + ensure!( + !is_bootc_container || !custom_options.emulated, + "--emulated is incompatible with bootable containers" + ); + + if let Some(size) = &custom_options.bootc_disk_size { + lazy_static! { + static ref SIZE_PATTERN: Regex = Regex::new(r"^[0-9]+[KMGT]?$").unwrap(); + } + + ensure!( + SIZE_PATTERN.is_match(size), + concat!( + "--bootc-disk-size value must be a number followed by an optional suffix K", + " (kilobyte, 1024), M (megabyte, 1024k), G (gigabyte, 1024M), or T (terabyte,", + " 1024G)", + ) + ); + + ensure!( + is_bootc_container, + "--bootc-disk-size only applies to bootable containers" + ); + } + + Ok(is_bootc_container) +} + fn is_first_create(spec: &oci_spec::runtime::Spec) -> Result<bool> { let path = spec.root_path()?.join("crun-vm/create-ran"); @@ -128,6 +211,7 @@ fn set_up_container_root( spec: &mut oci_spec::runtime::Spec, priv_dir_path: &Utf8Path, custom_options: &CustomOptions, + is_bootc_container: bool, ) -> Result<()> { let new_root_path = priv_dir_path.join("root"); fs::create_dir_all(&new_root_path)?; @@ -147,19 +231,22 @@ fn set_up_container_root( .unwrap(), )); - // set up container scripts + // set up container files #[derive(RustEmbed)] - #[folder = "scripts/"] - struct Scripts; + #[folder = "embed/"] + struct Embed; - for path in Scripts::iter() { + for path in Embed::iter() { let path_in_host = new_root_path.join("crun-vm").join(path.as_ref()); fs::create_dir_all(path_in_host.parent().unwrap())?; - let file = Scripts::get(&path).unwrap(); + let file = Embed::get(&path).unwrap(); fs::write(&path_in_host, file.data)?; - fs::set_permissions(&path_in_host, Permissions::from_mode(0o755))?; + + let is_script = path.as_ref().ends_with(".sh"); + let mode = if is_script { 0o755 } else { 0o644 }; + fs::set_permissions(&path_in_host, Permissions::from_mode(mode))?; } // configure container entrypoint @@ -169,14 +256,19 @@ fn set_up_container_root( } else if custom_options.print_config_json { vec!["cat", "/crun-vm/config.json"] } else { - vec!["/crun-vm/entrypoint.sh"] + let arg = if is_bootc_container { "1" } else { "0" }; + vec!["/crun-vm/entrypoint.sh", arg] }; spec.set_process({ let mut process = spec.process().clone().unwrap(); + process.set_cwd(".".into()); process.set_command_line(None); process.set_args(Some(command.into_iter().map(String::from).collect())); + + fix_selinux_label(&mut process); + Some(process) }); @@ -189,7 +281,21 @@ fn set_up_vm_image( priv_dir_path: &Utf8Path, custom_options: &CustomOptions, is_first_create: bool, + is_bootc_container: bool, ) -> Result<VmImageInfo> { + let mirror_vm_image_path_in_container = Utf8PathBuf::from("/crun-vm/image/image"); + let mirror_vm_image_path_in_host = spec.root_path()?.join("crun-vm/image/image"); + + if is_bootc_container { + // the image will be generated later + return Ok(VmImageInfo { + path: mirror_vm_image_path_in_container, + arch: None, + size: 0, + format: "qcow2".to_string(), + }); + } + // where inside the container to look for the VM image const VM_IMAGE_SEARCH_PATHS: [&str; 2] = ["./", "disk/"]; @@ -210,12 +316,9 @@ fn set_up_vm_image( fs::create_dir_all(&image_dir_path)?; if !image_dir_path.join("image").try_exists()? { - fs::hard_link(vm_image_path_in_host, image_dir_path.join("image"))?; + fs::hard_link(&vm_image_path_in_host, image_dir_path.join("image"))?; } - let mirror_vm_image_path_in_container = Utf8PathBuf::from("/crun-vm/image/image"); - let mirror_vm_image_path_in_host = spec.root_path()?.join("crun-vm/image/image"); - if custom_options.persistent { // Mount overlayfs to expose the user's VM image file with a different SELinux context so we // can always access it, using the file's parent as the upperdir so that writes still @@ -225,7 +328,7 @@ fn set_up_vm_image( bind_mount_dir_with_different_context( image_dir_path, mirror_vm_image_path_in_host.parent().unwrap(), - priv_dir_path.join("scratch"), + priv_dir_path.join("scratch-image"), spec.mount_label(), false, )?; @@ -236,7 +339,8 @@ fn set_up_vm_image( bind_mount_file(&mirror_vm_image_path_in_host, &mirror_vm_image_path_in_host)?; - let mut vm_image_info = VmImageInfo::of(&mirror_vm_image_path_in_host)?; + let mut vm_image_info = + VmImageInfo::of(&mirror_vm_image_path_in_host, custom_options.emulated)?; vm_image_info.path = mirror_vm_image_path_in_container; Ok(vm_image_info) @@ -248,7 +352,7 @@ fn set_up_vm_image( bind_mount_dir_with_different_context( image_dir_path, mirror_vm_image_path_in_host.parent().unwrap(), - priv_dir_path.join("scratch"), + priv_dir_path.join("scratch-image"), spec.mount_label(), true, )?; @@ -264,7 +368,8 @@ fn set_up_vm_image( let overlay_vm_image_path_in_container = Utf8Path::new("/").join(overlay_vm_image_path_in_container); - let mut base_vm_image_info = VmImageInfo::of(&mirror_vm_image_path_in_host)?; + let mut base_vm_image_info = + VmImageInfo::of(&vm_image_path_in_host, custom_options.emulated)?; base_vm_image_info.path = mirror_vm_image_path_in_container; if is_first_create { @@ -273,6 +378,7 @@ fn set_up_vm_image( Ok(VmImageInfo { path: Utf8Path::new("/").join(overlay_vm_image_path_in_container), + arch: base_vm_image_info.arch, size: base_vm_image_info.size, format: "qcow2".to_string(), }) @@ -575,7 +681,7 @@ fn set_up_security(spec: &mut oci_spec::runtime::Spec) { // TODO: This doesn't seem reasonable at all. Should we just force users to use a different // seccomp profile? Should passt provide the option to bypass a lot of the isolation that it // does, given we're already in a container *and* under a seccomp profile? - spec.linux_seccomp_syscalls_push( + spec.linux_seccomp_syscalls_push_front( oci_spec::runtime::LinuxSyscallBuilder::default() .names(["mount", "pivot_root", "umount2", "unshare"].map(String::from)) .action(oci_spec::runtime::LinuxSeccompAction::ScmpActAllow) @@ -623,7 +729,7 @@ fn set_up_first_boot_config( fn set_up_ssh_key_pair( spec: &mut oci_spec::runtime::Spec, custom_options: &CustomOptions, - env: RuntimeEnv, + engine: Engine, priv_dir_path: &Utf8Path, is_first_create: bool, ) -> Result<String> { @@ -641,7 +747,7 @@ fn set_up_ssh_key_pair( // - We're not running under Kubernetes (where there isn't a "host user"); and // - They have a key pair. let use_user_key_pair = !custom_options.random_ssh_key_pair - && env == RuntimeEnv::Other + && engine == Engine::Podman && user_ssh_dir.join("id_rsa.pub").is_file() && user_ssh_dir.join("id_rsa").is_file(); diff --git a/src/commands/create/runtime_env.rs b/src/commands/create/runtime_env.rs deleted file mode 100644 index 727802b..0000000 --- a/src/commands/create/runtime_env.rs +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -use std::fs; - -use anyhow::Result; -use camino::Utf8Path; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum RuntimeEnv { - Docker, - Kubernetes, - Other, -} - -impl RuntimeEnv { - pub fn current( - spec: &oci_spec::runtime::Spec, - original_root_path: impl AsRef<Utf8Path>, - ) -> Result<RuntimeEnv> { - let has_kubernetes_secrets_dir = spec.mounts().iter().flatten().any(|m| { - m.destination() - .starts_with("/var/run/secrets/kubernetes.io") - }); - - let has_kubernetes_managed_etc_hosts = spec - .mounts() - .iter() - .flatten() - .filter(|m| m.destination() == Utf8Path::new("/etc/hosts")) - .flat_map(|m| m.source()) - .next() - .map(fs::read_to_string) - .transpose()? - .and_then(|hosts| hosts.lines().next().map(|line| line.to_string())) - .map(|line| line.contains("Kubernetes-managed hosts file")) - .unwrap_or(false); - - let has_dockerenv_dot_file = original_root_path - .as_ref() - .join(".dockerenv") - .try_exists()?; - - if has_kubernetes_secrets_dir || has_kubernetes_managed_etc_hosts { - Ok(RuntimeEnv::Kubernetes) - } else if has_dockerenv_dot_file { - Ok(RuntimeEnv::Docker) - } else { - Ok(RuntimeEnv::Other) - } - } -} diff --git a/src/commands/exec.rs b/src/commands/exec.rs index 6f983a6..c7f035b 100644 --- a/src/commands/exec.rs +++ b/src/commands/exec.rs @@ -8,11 +8,13 @@ use std::io::{BufReader, BufWriter}; use anyhow::{bail, Result}; use clap::Parser; -use crate::util::crun; +use crate::util::{crun, fix_selinux_label}; pub fn exec(args: &liboci_cli::Exec, raw_args: &[impl AsRef<OsStr>]) -> Result<()> { assert!(args.command.is_empty()); + // load exec process config + let process_config_path = args.process.as_ref().expect("process config"); let mut process: oci_spec::runtime::Process = serde_json::from_reader(File::open(process_config_path).map(BufReader::new)?)?; @@ -22,11 +24,17 @@ pub fn exec(args: &liboci_cli::Exec, raw_args: &[impl AsRef<OsStr>]) -> Result<( let new_command = build_command(command)?; process.set_args(Some(new_command)); + fix_selinux_label(&mut process); + + // store modified exec process config + serde_json::to_writer( File::create(process_config_path).map(BufWriter::new)?, &process, )?; + // actually exec + crun(raw_args)?; Ok(()) diff --git a/src/util.rs b/src/util.rs index 107e9d6..2a75789 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,26 +1,46 @@ // SPDX-License-Identifier: GPL-2.0-or-later +use std::env; use std::ffi::{c_char, CString, OsStr}; use std::fs::{self, OpenOptions, Permissions}; -use std::io::{self, ErrorKind}; +use std::io::{self, ErrorKind, Write}; use std::os::unix::ffi::OsStrExt; use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::process::{Command, Stdio}; +use std::str; use anyhow::{anyhow, bail, ensure, Result}; use camino::{Utf8Path, Utf8PathBuf}; use nix::mount::{MntFlags, MsFlags}; use serde::Deserialize; +// When the container image's entrypoint is /sbin/init or similar, Podman gives the entrypoint (and +// exec entrypoint) process an SELinux label of, for instance: +// +// system_u:system_r:container_init_t:s0:c276,c638 +// +// However, we are going to change our entrypoint to something else, so we need to use the +// "standard" label that Podman otherwise gives, which in this case would be: +// +// system_u:system_r:container_t:s0:c276,c638 +// +// This function performs that mapping. +pub fn fix_selinux_label(process: &mut oci_spec::runtime::Process) { + if let Some(label) = process.selinux_label() { + let new_label = label.replace("container_init_t", "container_t"); + process.set_selinux_label(Some(new_label)); + } +} + pub fn set_file_context(path: impl AsRef<Utf8Path>, context: &str) -> Result<()> { extern "C" { - fn setfilecon(path: *const c_char, con: *const c_char) -> i32; + fn lsetfilecon(path: *const c_char, con: *const c_char) -> i32; } let path = CString::new(path.as_ref().as_os_str().as_bytes())?; let context = CString::new(context.as_bytes())?; - if unsafe { setfilecon(path.as_ptr(), context.as_ptr()) } != 0 { + if unsafe { lsetfilecon(path.as_ptr(), context.as_ptr()) } != 0 { return Err(io::Error::last_os_error().into()); } @@ -179,7 +199,7 @@ pub trait SpecExt { linux_device_cgroup: oci_spec::runtime::LinuxDeviceCgroup, ); fn process_capabilities_insert_beip(&mut self, capability: oci_spec::runtime::Capability); - fn linux_seccomp_syscalls_push(&mut self, linux_syscall: oci_spec::runtime::LinuxSyscall); + fn linux_seccomp_syscalls_push_front(&mut self, linux_syscall: oci_spec::runtime::LinuxSyscall); } impl SpecExt for oci_spec::runtime::Spec { @@ -257,7 +277,10 @@ impl SpecExt for oci_spec::runtime::Spec { }); } - fn linux_seccomp_syscalls_push(&mut self, linux_syscall: oci_spec::runtime::LinuxSyscall) { + fn linux_seccomp_syscalls_push_front( + &mut self, + linux_syscall: oci_spec::runtime::LinuxSyscall, + ) { self.set_linux({ let mut linux = self.linux().clone().expect("linux config"); linux.set_seccomp({ @@ -265,7 +288,7 @@ impl SpecExt for oci_spec::runtime::Spec { if let Some(seccomp) = &mut seccomp { seccomp.set_syscalls({ let mut syscalls = seccomp.syscalls().clone().unwrap_or_default(); - syscalls.push(linux_syscall); + syscalls.insert(0, linux_syscall); Some(syscalls) }); } @@ -314,6 +337,9 @@ pub struct VmImageInfo { #[serde(skip)] pub path: Utf8PathBuf, + #[serde(skip)] + pub arch: Option<String>, + #[serde(rename = "virtual-size")] pub size: u64, @@ -321,7 +347,7 @@ pub struct VmImageInfo { } impl VmImageInfo { - pub fn of(vm_image_path: impl AsRef<Utf8Path>) -> Result<VmImageInfo> { + pub fn of(vm_image_path: impl AsRef<Utf8Path>, identify_arch: bool) -> Result<VmImageInfo> { let vm_image_path = vm_image_path.as_ref().to_path_buf(); let output = Command::new("qemu-img") @@ -340,6 +366,12 @@ impl VmImageInfo { let mut info: VmImageInfo = serde_json::from_slice(&output.stdout)?; info.path = vm_image_path; + if identify_arch { + info.arch = identify_image_arch(&info.path)? + .ok_or_else(|| anyhow!("Could not identify VM image architecture"))? + .into(); + } + Ok(info) } } @@ -371,6 +403,62 @@ pub fn create_overlay_vm_image( Ok(()) } +pub fn identify_image_arch(image_path: impl AsRef<Utf8Path>) -> Result<Option<String>> { + let xml = virt_inspector([ + "--add", + image_path.as_ref().as_str(), + "--no-applications", + "--no-icon", + ])?; + + xpath(&xml, "string(//arch)") +} + +fn virt_inspector(args: impl IntoIterator<Item = impl AsRef<OsStr>>) -> Result<String> { + let cache_dir = format!("/var/tmp/crun-vm-{}", env::var("_CONTAINERS_ROOTLESS_UID")?); + fs::create_dir_all(&cache_dir)?; + + let output = Command::new("virt-inspector") + .args(args) + .env("LIBGUESTFS_BACKEND", "direct") + .env("LIBGUESTFS_CACHEDIR", &cache_dir) + .output()?; + + ensure!( + output.status.success(), + "virt-inspector failed: {}", + String::from_utf8_lossy(&output.stderr), + ); + + Ok(String::from_utf8(output.stdout)?) +} + +fn xpath(xml: &str, path: &str) -> Result<Option<String>> { + let mut child = Command::new("virt-inspector") + .arg("--xpath") + .arg(path) + .stdin(Stdio::piped()) + .spawn()?; + + child.stdin.take().unwrap().write_all(xml.as_bytes())?; + + let output = child.wait_with_output()?; + + ensure!( + output.status.success(), + "virt-inspector --xpath failed: {}", + String::from_utf8_lossy(&output.stderr), + ); + + let result = String::from_utf8(output.stdout)?; + + if result.is_empty() { + Ok(None) + } else { + Ok(Some(result)) + } +} + /// Run `crun`. /// /// `crun` will inherit this process' standard streams. diff --git a/tests/env.sh b/tests/env.sh index f6ed1d2..ee9a874 100755 --- a/tests/env.sh +++ b/tests/env.sh @@ -13,18 +13,21 @@ declare -A TEST_IMAGES TEST_IMAGES=( [fedora]=quay.io/containerdisks/fedora:40 # uses cloud-init [coreos]=quay.io/crun-vm/example-fedora-coreos:40 # uses Ignition + [fedora-bootc]=quay.io/fedora/fedora-bootc:40 # bootable container ) declare -A TEST_IMAGES_DEFAULT_USER TEST_IMAGES_DEFAULT_USER=( [fedora]=fedora [coreos]=core + [fedora-bootc]=cloud-user ) declare -A TEST_IMAGES_DEFAULT_USER_HOME TEST_IMAGES_DEFAULT_USER_HOME=( [fedora]=/home/fedora [coreos]=/var/home/core + [fedora-bootc]=/var/home/cloud-user ) __bad_usage() { @@ -140,12 +143,12 @@ build) # expand base image - __log_and_run qemu-img create -f qcow2 "$temp_dir/resized-image.qcow2" 20G + __log_and_run qemu-img create -f qcow2 "$temp_dir/image.qcow2" 50G __log_and_run virt-resize \ --quiet \ --expand /dev/sda4 \ "$temp_dir/image" \ - "$temp_dir/resized-image.qcow2" + "$temp_dir/image.qcow2" rm "$temp_dir/image" @@ -179,6 +182,7 @@ build) bash \ coreutils \ crun \ + crun-krun \ docker \ genisoimage \ grep \ @@ -210,17 +214,12 @@ build) __log_and_run podman wait --ignore "$container_name-build" __extra_cleanup() { :; } - __log_and_run virt-sparsify \ - --quiet \ - "$temp_dir/resized-image.qcow2" \ - "$temp_dir/final-image.qcow2" - - rm "$temp_dir/resized-image.qcow2" + __log_and_run virt-sparsify --quiet --in-place "$temp_dir/image.qcow2" # package new image file __log_and_run "$( __rel "$repo_root/util/package-vm-image.sh" )" \ - "$temp_dir/final-image.qcow2" \ + "$temp_dir/image.qcow2" \ "$env_image" __big_log 33 'Done.' @@ -393,6 +392,7 @@ run) } TEMP_DIR=~/$label.temp UTIL_DIR=~/$label.util + TEST_ID=$label ENGINE=$engine export RUST_BACKTRACE=1 RUST_LIB_BACKTRACE=1 $( cat "$t" )\ diff --git a/tests/t/bootc-disk-size.sh b/tests/t/bootc-disk-size.sh new file mode 100644 index 0000000..157cf03 --- /dev/null +++ b/tests/t/bootc-disk-size.sh @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +image="${TEST_IMAGES[fedora-bootc]}" +user="${TEST_IMAGES_DEFAULT_USER[fedora-bootc]}" + +__run() { + __engine run --detach --name "$TEST_ID" "$image" --bootc-disk-size "$1" +} + +! RUST_LIB_BACKTRACE=0 __run 0 +__engine rm "$TEST_ID" + +for size in 1K 1M; do + __run "$size" + ! __engine exec "$TEST_ID" --as "$user" + __engine rm --force "$TEST_ID" +done + +for size in 1G 1T 1024T; do + __run "$size" + __engine exec "$TEST_ID" --as "$user" + __engine rm --force "$TEST_ID" +done diff --git a/tests/t/bootc-rootfs.sh b/tests/t/bootc-rootfs.sh new file mode 100644 index 0000000..f5a1700 --- /dev/null +++ b/tests/t/bootc-rootfs.sh @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +"$UTIL_DIR/extract-vm-image.sh" "${TEST_IMAGES[fedora-bootc]}" "$TEMP_DIR/image" + +__run() { + __engine run --rm --detach --name "$TEST_ID" "$@" --rootfs "$TEMP_DIR" +} + +! __run +! __run --persistent diff --git a/tests/t/cloud-init.sh b/tests/t/cloud-init.sh index 6ea51dd..69575d2 100644 --- a/tests/t/cloud-init.sh +++ b/tests/t/cloud-init.sh @@ -1,30 +1,36 @@ # SPDX-License-Identifier: GPL-2.0-or-later -image="${TEST_IMAGES[fedora]}" -user="${TEST_IMAGES_DEFAULT_USER[fedora]}" -home="${TEST_IMAGES_DEFAULT_USER_HOME[fedora]}" - -cat >"$TEMP_DIR/user-data" <<EOF -#cloud-config -write_files: - - path: $home/file - content: | - hello +for os in fedora fedora-bootc; do + + image="${TEST_IMAGES[$os]}" + user="${TEST_IMAGES_DEFAULT_USER[$os]}" + home="${TEST_IMAGES_DEFAULT_USER_HOME[$os]}" + + cat >"$TEMP_DIR/user-data" <<-EOF + #cloud-config + write_files: + - path: $home/file + content: | + hello EOF -cat >"$TEMP_DIR/meta-data" <<EOF + cat >"$TEMP_DIR/meta-data" <<-EOF EOF -__engine run \ - --rm --detach \ - --name cloud-init \ - "$image" \ - --cloud-init "$TEMP_DIR" + __engine run \ + --rm --detach \ + --name "$TEST_ID" \ + "$image" \ + --cloud-init "$TEMP_DIR" + + __test() { + __engine exec "$TEST_ID" --as "$user" "cmp $home/file <<< hello" + } + + __test + __engine restart "$TEST_ID" + __test -__test() { - __engine exec cloud-init --as "$user" "cmp $home/file <<< hello" -} + __engine stop "$TEST_ID" -__test -__engine restart cloud-init -__test +done diff --git a/tests/t/emulated.sh b/tests/t/emulated.sh index 0b6675a..7cffb4b 100644 --- a/tests/t/emulated.sh +++ b/tests/t/emulated.sh @@ -1,4 +1,4 @@ # SPDX-License-Identifier: GPL-2.0-or-later -__engine run --detach --name emulated "${TEST_IMAGES[fedora]}" --emulated -__engine exec emulated --as fedora +__engine run --detach --name "$TEST_ID" "${TEST_IMAGES[fedora]}" --emulated +__engine exec "$TEST_ID" --as fedora diff --git a/tests/t/hostname.sh b/tests/t/hostname.sh index 3b1039c..9879af2 100644 --- a/tests/t/hostname.sh +++ b/tests/t/hostname.sh @@ -1,42 +1,42 @@ # SPDX-License-Identifier: GPL-2.0-or-later -for os in fedora coreos; do +for os in "${!TEST_IMAGES[@]}"; do image="${TEST_IMAGES[$os]}" user="${TEST_IMAGES_DEFAULT_USER[$os]}" # default hostname - id=$( __engine run --rm --detach --name "hostname-$os-default" "$image" ) + id=$( __engine run --rm --detach --name "$TEST_ID-$os-default" "$image" ) __test() { - __engine exec "hostname-$os-default" --as "$user" \ + __engine exec "$TEST_ID-$os-default" --as "$user" \ "set -x && [[ \$( hostname ) == ${id::12} ]]" } __test - __engine restart "hostname-$os-default" + __engine restart "$TEST_ID-$os-default" __test - __engine stop --time 0 "hostname-$os-default" + __engine stop --time 0 "$TEST_ID-$os-default" # custom hostname __engine run \ --rm --detach \ - --name "hostname-$os-custom" \ + --name "$TEST_ID-$os-custom" \ --hostname my-test-vm \ "$image" __test() { - __engine exec "hostname-$os-custom" --as "$user" \ + __engine exec "$TEST_ID-$os-custom" --as "$user" \ "set -x && [[ \$( hostname ) == my-test-vm ]]" } __test - __engine restart "hostname-$os-custom" + __engine restart "$TEST_ID-$os-custom" __test - __engine stop --time 0 "hostname-$os-custom" + __engine stop --time 0 "$TEST_ID-$os-custom" done diff --git a/tests/t/ignition.sh b/tests/t/ignition.sh index d2cab70..e2cfb9c 100644 --- a/tests/t/ignition.sh +++ b/tests/t/ignition.sh @@ -26,14 +26,14 @@ EOF __engine run \ --rm --detach \ - --name ignition \ + --name "$TEST_ID" \ "$image" \ --ignition "$TEMP_DIR/config.ign" __test() { - __engine exec ignition --as "$user" "cmp $home/file <<< hello" + __engine exec "$TEST_ID" --as "$user" "cmp $home/file <<< hello" } __test -__engine restart ignition +__engine restart "$TEST_ID" __test diff --git a/tests/t/mount.sh b/tests/t/mount.sh index cebb2e5..726b596 100644 --- a/tests/t/mount.sh +++ b/tests/t/mount.sh @@ -1,6 +1,6 @@ # SPDX-License-Identifier: GPL-2.0-or-later -for os in fedora coreos; do +for os in "${!TEST_IMAGES[@]}"; do image="${TEST_IMAGES[$os]}" user="${TEST_IMAGES_DEFAULT_USER[$os]}" @@ -10,37 +10,37 @@ for os in fedora coreos; do __engine run \ --rm --detach \ - --name "mount-$os" \ + --name "$TEST_ID-$os" \ --volume "$TEMP_DIR/file:$home/file:z" \ --volume "$TEMP_DIR:$home/dir:z" \ --mount "type=tmpfs,dst=$home/tmp" \ "$image" __test() { - __engine exec "mount-$os" --as "$user" + __engine exec "$TEST_ID-$os" --as "$user" - __engine exec "mount-$os" --as "$user" " + __engine exec "$TEST_ID-$os" --as "$user" " set -e [[ -b $home/file ]] sudo cmp -n 6 $home/file <<< hello " - __engine exec "mount-$os" --as "$user" " + __engine exec "$TEST_ID-$os" --as "$user" " set -e mount -l | grep '^virtiofs-0 on $home/dir type virtiofs' [[ -d $home/dir ]] sudo cmp $home/dir/file <<< hello " - __engine exec "mount-$os" --as "$user" " + __engine exec "$TEST_ID-$os" --as "$user" " mount -l | grep '^tmpfs on $home/tmp type tmpfs' " } __test - __engine restart "mount-$os" + __engine restart "$TEST_ID-$os" __test - __engine stop --time 0 "mount-$os" + __engine stop --time 0 "$TEST_ID-$os" done diff --git a/tests/t/persistent.sh b/tests/t/persistent.sh index 0f8c266..e850031 100644 --- a/tests/t/persistent.sh +++ b/tests/t/persistent.sh @@ -9,14 +9,14 @@ fi # Usage: __run <crun_vm_option> [<extra_podman_options...>] __run() { - __engine run --rm --detach --name persistent "${@:2}" --rootfs "$TEMP_DIR" "$1" + __engine run --rm --detach --name "$TEST_ID" "${@:2}" --rootfs "$TEMP_DIR" "$1" } # Usage: __test <crun_vm_option> <condition> __test() { id=$( __run "$1" ) - __engine exec persistent --as fedora "$2" - __engine stop persistent + __engine exec "$TEST_ID" --as fedora "$2" + __engine stop "$TEST_ID" if [[ "$ENGINE" != rootful-podman ]]; then # ensure user that invoked `engine run` can delete crun-vm state @@ -34,4 +34,4 @@ __test "" '[[ -e i-was-here ]]' ! RUST_LIB_BACKTRACE=0 __run --persistent --read-only __run "" --read-only -__engine exec persistent --as fedora +__engine exec "$TEST_ID" --as fedora diff --git a/tests/t/publish.sh b/tests/t/publish.sh index 4c9b98a..07213d9 100644 --- a/tests/t/publish.sh +++ b/tests/t/publish.sh @@ -1,30 +1,33 @@ # SPDX-License-Identifier: GPL-2.0-or-later -image="${TEST_IMAGES[fedora]}" -user="${TEST_IMAGES_DEFAULT_USER[fedora]}" +trap '__engine stop "$TEST_ID"' EXIT -__engine run \ - --rm --detach \ - --name publish \ - --publish 127.0.0.1::8000 \ - "$image" +for os in fedora fedora-bootc; do -endpoint=$( __engine port publish | tee /dev/stderr | cut -d' ' -f3 ) + image="${TEST_IMAGES[$os]}" + user="${TEST_IMAGES_DEFAULT_USER[$os]}" -__engine exec publish --as "$user" + __engine run --rm --detach --name "$TEST_ID" --publish 127.0.0.1::8000 "$image" -__log 'Ensuring curl fails...' -! curl "$endpoint" 2>/dev/null + endpoint=$( __engine port "$TEST_ID" | tee /dev/stderr | cut -d' ' -f3 ) -__engine exec publish --as "$user" python -m http.server & -trap '__engine stop publish' EXIT + __engine exec "$TEST_ID" --as "$user" -__log 'Ensuring curl succeeds...' + __log 'Ensuring curl fails...' + ! curl "$endpoint" 2>/dev/null -i=0 -max_tries=30 + __engine exec "$TEST_ID" --as "$user" python -m http.server & + + __log 'Ensuring curl succeeds...' + + i=0 + max_tries=30 + + until [[ "$( curl "$endpoint" 2>/dev/null )" == '<!DOCTYPE HTML>'* ]]; do + (( ++i < max_tries )) + sleep 1 + done + + __engine stop "$TEST_ID" -until [[ "$( curl "$endpoint" 2>/dev/null )" == '<!DOCTYPE HTML>'* ]]; do - (( ++i < max_tries )) - sleep 1 done diff --git a/tests/t/random-ssh-key-pair.sh b/tests/t/random-ssh-key-pair.sh index fcd7afd..3fe4c1d 100644 --- a/tests/t/random-ssh-key-pair.sh +++ b/tests/t/random-ssh-key-pair.sh @@ -2,10 +2,10 @@ __engine run \ --detach \ - --name random-ssh-key-pair \ + --name "$TEST_ID" \ "${TEST_IMAGES[fedora]}" \ --random-ssh-key-pair -__engine exec random-ssh-key-pair --as fedora -__engine restart random-ssh-key-pair -__engine exec random-ssh-key-pair --as fedora +__engine exec "$TEST_ID" --as fedora +__engine restart "$TEST_ID" +__engine exec "$TEST_ID" --as fedora diff --git a/tests/t/stop-start.sh b/tests/t/stop-start.sh index bdce535..d0e47fb 100644 --- a/tests/t/stop-start.sh +++ b/tests/t/stop-start.sh @@ -1,14 +1,14 @@ # SPDX-License-Identifier: GPL-2.0-or-later -__engine run --detach --name stop-start "${TEST_IMAGES[fedora]}" +__engine run --detach --name "$TEST_ID" "${TEST_IMAGES[fedora]}" -__engine exec stop-start --as fedora '[[ ! -e i-was-here ]] && touch i-was-here' +__engine exec "$TEST_ID" --as fedora '[[ ! -e i-was-here ]] && touch i-was-here' for (( i = 0; i < 2; ++i )); do - __engine stop stop-start - __engine start stop-start + __engine stop "$TEST_ID" + __engine start "$TEST_ID" - __engine exec stop-start --as fedora '[[ -e i-was-here ]]' + __engine exec "$TEST_ID" --as fedora '[[ -e i-was-here ]]' done