Add support for running bootc bootable containers
We attempt to detect if a container image is bootable. We can't easily retrieve the image's labels, so we check if /usr/lib/bootc/install exists and is a directory. If so, it is a bootable container. If it is a bootable container but we're not running under Podman, we fail with an error. Once our container's entrypoint starts running, a background process on the host (outside the container) queries Podman for the image's name and ID, which the OCI runtime does not get but bootc-install needs. It then saves the container image as an OCI archive. It then runs the original container to generate the VM image. We do this using krun [1] so that elevated privileges aren't necessary. Our entrypoint blocks until this is done, and all subsequent logic remains the same. We could potentially avoid the OCI archive creation step by mounting the host's container storage into the container running under krun. This isn't trivial to achieve due to SELinux label and context mismatches between the host and the krun environment, so we leave this optimization for a future date. Closes #26. [1] https://github.com/containers/crun/blob/main/krun.1.md Signed-off-by: Alberto Faria <afaria@redhat.com>
This commit is contained in:
parent
7a649c3bef
commit
4b98cd9c48
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -96,6 +96,21 @@ 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.
|
||||
|
||||
## First-boot customization
|
||||
|
||||
### cloud-init
|
||||
|
|
@ -320,6 +335,9 @@ 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.
|
||||
|
||||
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
|
||||
|
||||
crun-vm internally uses [libvirt] to launch a VM, generating a [domain XML
|
||||
|
|
@ -340,6 +358,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/
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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 oci-archive:/output/image.oci-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/success
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
set -o errexit -o pipefail -o nounset
|
||||
|
||||
original_root=$1
|
||||
priv_dir=$2
|
||||
container_id=$3
|
||||
|
||||
__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*
|
||||
|
||||
__step 'Storing the container image as an OCI archive...'
|
||||
|
||||
image_info=$(
|
||||
podman container inspect \
|
||||
--format '{{.ImageName}}\t{{.Image}}' \
|
||||
"$container_id"
|
||||
)
|
||||
|
||||
image_name=$( cut -f1 <<< "$image_info" )
|
||||
image_id=$( cut -f2 <<< "$image_info" )
|
||||
|
||||
oci_archive=$bootc_dir/image.oci-archive
|
||||
|
||||
# save container *image* as an OCI archive
|
||||
|
||||
podman save --format oci-archive --output "$oci_archive.tmp" "$image_id"
|
||||
mv "$oci_archive.tmp" "$oci_archive"
|
||||
|
||||
# adjust krun config
|
||||
|
||||
__step 'Generating a VM image from the container image...'
|
||||
|
||||
__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
|
||||
|
||||
truncate --size 10G "$bootc_dir/image.raw" # TODO: allow adjusting disk size
|
||||
|
||||
krun run \
|
||||
--config "$bootc_dir/config.json" \
|
||||
"crun-vm-$container_id" \
|
||||
</dev/ptmx
|
||||
|
||||
[[ -e "$bootc_dir/success" ]]
|
||||
|
||||
__step 'Booting VM...'
|
||||
|
|
@ -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.raw /crun-vm/image/image
|
||||
|
||||
fi
|
||||
|
||||
# launch VM
|
||||
|
||||
function __bg_ensure_tty() {
|
||||
|
|
@ -11,6 +11,7 @@ prepare:
|
|||
- cargo
|
||||
- coreutils
|
||||
- crun
|
||||
- crun-krun
|
||||
- docker
|
||||
- genisoimage
|
||||
- grep
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ 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;
|
||||
|
|
@ -32,29 +34,25 @@ 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)?;
|
||||
ensure_unprivileged(&spec)?;
|
||||
|
||||
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!(
|
||||
!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 runtime_env = RuntimeEnv::current(&spec, &original_root_path)?;
|
||||
let custom_options = CustomOptions::from_spec(&spec, runtime_env)?;
|
||||
|
||||
let is_bootc_container = is_bootc_container(
|
||||
&args.container_id,
|
||||
bundle_path,
|
||||
&original_root_path,
|
||||
runtime_env,
|
||||
)?;
|
||||
|
||||
ensure!(
|
||||
!is_bootc_container || !custom_options.emulated,
|
||||
"--emulated is incompatible with bootable containers"
|
||||
);
|
||||
|
||||
// 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));
|
||||
fs::create_dir_all(&priv_dir_path)?;
|
||||
|
|
@ -66,7 +64,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(
|
||||
|
|
@ -75,6 +79,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();
|
||||
|
|
@ -105,9 +110,84 @@ 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(&original_root_path)
|
||||
.arg(&priv_dir_path)
|
||||
.arg(&args.container_id)
|
||||
.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(
|
||||
container_id: &str,
|
||||
bundle_path: &Utf8Path,
|
||||
original_root_path: &Utf8Path,
|
||||
env: RuntimeEnv,
|
||||
) -> Result<bool> {
|
||||
lazy_static! {
|
||||
static ref PATTERN: Regex = Regex::new(r"/overlay-containers/([^/]+)/userdata$").unwrap();
|
||||
}
|
||||
|
||||
let is_bootc_container = original_root_path.join("usr/lib/bootc/install").is_dir();
|
||||
|
||||
if is_bootc_container {
|
||||
// check as much as we can that we're running under podman
|
||||
|
||||
let is_podman_bundle_path = match PATTERN.captures(bundle_path.as_str()) {
|
||||
Some(captures) => &captures[1] == container_id,
|
||||
None => false,
|
||||
};
|
||||
|
||||
ensure!(
|
||||
env == RuntimeEnv::Other && is_podman_bundle_path,
|
||||
"bootc containers are only supported with Podman"
|
||||
);
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
|
|
@ -129,6 +209,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)?;
|
||||
|
|
@ -148,19 +229,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
|
||||
|
|
@ -170,7 +254,8 @@ 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({
|
||||
|
|
@ -194,7 +279,20 @@ 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,
|
||||
size: 0,
|
||||
format: "raw".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// where inside the container to look for the VM image
|
||||
const VM_IMAGE_SEARCH_PATHS: [&str; 2] = ["./", "disk/"];
|
||||
|
||||
|
|
@ -218,9 +316,6 @@ fn set_up_vm_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
|
||||
|
|
@ -230,7 +325,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,
|
||||
)?;
|
||||
|
|
@ -253,7 +348,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,
|
||||
)?;
|
||||
|
|
@ -580,7 +675,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)
|
||||
|
|
|
|||
13
src/util.rs
13
src/util.rs
|
|
@ -33,13 +33,13 @@ pub fn fix_selinux_label(process: &mut oci_spec::runtime::Process) {
|
|||
|
||||
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());
|
||||
}
|
||||
|
||||
|
|
@ -198,7 +198,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 {
|
||||
|
|
@ -276,7 +276,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({
|
||||
|
|
@ -284,7 +287,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)
|
||||
});
|
||||
}
|
||||
|
|
|
|||
17
tests/env.sh
17
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.'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
if [[ "$ENGINE" == docker ]]; then
|
||||
# we only support bootc containers under Podman
|
||||
__skip
|
||||
fi
|
||||
|
||||
"$UTIL_DIR/extract-vm-image.sh" "${TEST_IMAGES[fedora-bootc]}" "$TEMP_DIR/image"
|
||||
|
||||
__run() {
|
||||
__engine run --rm --detach --name bootc-rootfs "$@" --rootfs "$TEMP_DIR"
|
||||
}
|
||||
|
||||
! __run
|
||||
! __run --persistent
|
||||
|
|
@ -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]}"
|
||||
for os in fedora fedora-bootc; do
|
||||
|
||||
cat >"$TEMP_DIR/user-data" <<EOF
|
||||
#cloud-config
|
||||
write_files:
|
||||
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 \
|
||||
__engine run \
|
||||
--rm --detach \
|
||||
--name cloud-init \
|
||||
"$image" \
|
||||
--cloud-init "$TEMP_DIR"
|
||||
|
||||
__test() {
|
||||
__test() {
|
||||
__engine exec cloud-init --as "$user" "cmp $home/file <<< hello"
|
||||
}
|
||||
}
|
||||
|
||||
__test
|
||||
__engine restart cloud-init
|
||||
__test
|
||||
__test
|
||||
__engine restart cloud-init
|
||||
__test
|
||||
|
||||
__engine stop cloud-init
|
||||
|
||||
done
|
||||
|
|
|
|||
|
|
@ -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]}"
|
||||
|
|
|
|||
|
|
@ -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]}"
|
||||
|
|
|
|||
|
|
@ -1,30 +1,33 @@
|
|||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
image="${TEST_IMAGES[fedora]}"
|
||||
user="${TEST_IMAGES_DEFAULT_USER[fedora]}"
|
||||
|
||||
__engine run \
|
||||
--rm --detach \
|
||||
--name publish \
|
||||
--publish 127.0.0.1::8000 \
|
||||
"$image"
|
||||
|
||||
endpoint=$( __engine port publish | tee /dev/stderr | cut -d' ' -f3 )
|
||||
|
||||
__engine exec publish --as "$user"
|
||||
|
||||
__log 'Ensuring curl fails...'
|
||||
! curl "$endpoint" 2>/dev/null
|
||||
|
||||
__engine exec publish --as "$user" python -m http.server &
|
||||
trap '__engine stop publish' EXIT
|
||||
|
||||
__log 'Ensuring curl succeeds...'
|
||||
for os in fedora fedora-bootc; do
|
||||
|
||||
i=0
|
||||
max_tries=30
|
||||
image="${TEST_IMAGES[$os]}"
|
||||
user="${TEST_IMAGES_DEFAULT_USER[$os]}"
|
||||
|
||||
until [[ "$( curl "$endpoint" 2>/dev/null )" == '<!DOCTYPE HTML>'* ]]; do
|
||||
__engine run --rm --detach --name publish --publish 127.0.0.1::8000 "$image"
|
||||
|
||||
endpoint=$( __engine port publish | tee /dev/stderr | cut -d' ' -f3 )
|
||||
|
||||
__engine exec publish --as "$user"
|
||||
|
||||
__log 'Ensuring curl fails...'
|
||||
! curl "$endpoint" 2>/dev/null
|
||||
|
||||
__engine exec publish --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 publish
|
||||
|
||||
done
|
||||
|
|
|
|||
Loading…
Reference in New Issue