From 1db3040462e3f7a93adc73ad69f23c01019bc54c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Budai?= Date: Thu, 14 Dec 2023 13:07:29 +0100 Subject: [PATCH] install: add --source-imgref MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bootc install and install-to-filesystem currently rely on the fact that they run inside a podman container. That's quite inconvenient for using bootc for osbuild, because osbuild already run everything in a container. While having a container in a container is surely possible, it gets quite messy. Instead of going this route, this commit implements a new --source-imgref argument. --source-imgref accepts a container image reference (the same one that skopeo uses). When --source-imgref is used, bootc doesn't escape the container to fetch the container image from host's container storage. Instead, the container image given by --source-imgref is used. Even when running in this mode, bootc needs to run in a container created from the same container image that is passed using --source-imgref. However, this isn't a problem to do in osbuild. This really just removes the need for bootc to escape the container to the host mount namespace. Signed-off-by: Ondřej Budai --- docs/install.md | 15 +++++ lib/src/install.rs | 141 ++++++++++++++++++++++++++++++++------------- 2 files changed, 116 insertions(+), 40 deletions(-) diff --git a/docs/install.md b/docs/install.md index e18fc66ce..92b973edf 100644 --- a/docs/install.md +++ b/docs/install.md @@ -234,3 +234,18 @@ At the current time, leftover data in `/` is **NOT** automatically cleaned up. be useful, because it allows the new image to automatically import data from the previous host system! For example, things like SSH keys or container images can be copied and then deleted from the original. + +### Using `bootc install to-filesystem --source-imgref ` + +By default, `bootc install` has to be run inside a podman container. With this assumption, +it can escape the container, find the source container image (including its layers) in +the podman's container storage and use it to create the image. + +When `--source-imgref ` is given, `bootc` no longer assumes that it runs inside podman. +Instead, the given container image reference (see [skopeo(1)](https://github.com/containers/skopeo/blob/main/docs/skopeo.1.md) +for accepted formats) is used to fetch the image. Note that `bootc install` still has to be +run inside a chroot created from the container image. However, this allows users to use +a different sandboxing tool (e.g. [bubblewrap](https://github.com/containers/bubblewrap)). + +This argument is mainly useful for 3rd-party tooling for building disk images from bootable +containers (e.g. based on [osbuild](https://github.com/osbuild/osbuild)). diff --git a/lib/src/install.rs b/lib/src/install.rs index 06cc811f4..3a26db230 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -102,6 +102,18 @@ pub(crate) struct InstallTargetOpts { pub(crate) skip_fetch_check: bool, } +#[derive(clap::Args, Debug, Clone, Serialize, Deserialize)] +pub(crate) struct InstallSourceOpts { + /// Install the system from an explicitly given source. + /// + /// By default, bootc install and install-to-filesystem assumes that it runs in a podman container, and + /// it takes the container image to install from the podman's container registry. + /// If --source-imgref is given, bootc uses it as the installation source, instead of the behaviour explained + /// in the previous paragraph. See skopeo(1) for accepted formats. + #[clap(long)] + pub(crate) source_imgref: Option, +} + #[derive(clap::Args, Debug, Clone, Serialize, Deserialize)] pub(crate) struct InstallConfigOpts { /// Disable SELinux in the target (installed) system. @@ -137,6 +149,10 @@ pub(crate) struct InstallToDiskOpts { #[serde(flatten)] pub(crate) block_opts: InstallBlockDeviceOpts, + #[clap(flatten)] + #[serde(flatten)] + pub(crate) source_opts: InstallSourceOpts, + #[clap(flatten)] #[serde(flatten)] pub(crate) target_opts: InstallTargetOpts, @@ -209,6 +225,9 @@ pub(crate) struct InstallToFilesystemOpts { #[clap(flatten)] pub(crate) filesystem_opts: InstallTargetFilesystemOpts, + #[clap(flatten)] + pub(crate) source_opts: InstallSourceOpts, + #[clap(flatten)] pub(crate) target_opts: InstallTargetOpts, @@ -225,6 +244,15 @@ pub(crate) struct SourceInfo { pub(crate) digest: Option, /// Whether or not SELinux appears to be enabled in the source commit pub(crate) selinux: bool, + /// Whether the source is available in the host mount namespace + pub(crate) in_host_mountns: Option, +} + +/// Information about the host mount namespace +#[derive(Debug, Clone)] +pub(crate) struct HostMountnsInfo { + /// True if the skoepo on host supports containers-storage: + pub(crate) skopeo_supports_containers_storage: bool, } // Shared read-only global state @@ -232,8 +260,6 @@ pub(crate) struct State { pub(crate) source: SourceInfo, /// Force SELinux off in target system pub(crate) override_disable_selinux: bool, - /// True if the skoepo on host supports containers-storage: - pub(crate) skopeo_supports_containers_storage: bool, #[allow(dead_code)] pub(crate) setenforce_guard: Option, #[allow(dead_code)] @@ -368,6 +394,29 @@ impl SourceInfo { name: container_info.image.clone(), }; let digest = crate::podman::imageid_to_digest(&container_info.imageid)?; + + let skopeo_supports_containers_storage = skopeo_supports_containers_storage() + .context("Failed to run skopeo (it currently must be installed in the host root)")?; + Self::from( + imageref, + Some(digest), + Some(HostMountnsInfo { + skopeo_supports_containers_storage, + }), + ) + } + + #[context("Creating source info from a given imageref")] + pub(crate) fn from_imageref(imageref: &str) -> Result { + let imageref = ostree_container::ImageReference::try_from(imageref)?; + Self::from(imageref, None, None) + } + + fn from( + imageref: ostree_container::ImageReference, + digest: Option, + in_host_mountns: Option, + ) -> Result { let cancellable = ostree::gio::Cancellable::NONE; let commit = Task::new("Reading ostree commit", "ostree") .args(["--repo=/ostree/repo", "rev-parse", "--single"]) @@ -384,8 +433,9 @@ impl SourceInfo { let selinux = crate::lsm::xattrs_have_selinux(&xattrs); Ok(Self { imageref, - digest: Some(digest), + digest, selinux, + in_host_mountns, }) } } @@ -562,32 +612,39 @@ async fn initialize_ostree_root_from_self( let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs))); sysroot.load(cancellable)?; - // We need to fetch the container image from the root mount namespace - let skopeo_cmd = run_in_host_mountns("skopeo"); - let proxy_cfg = ostree_container::store::ImageProxyConfig { - skopeo_cmd: Some(skopeo_cmd), - ..Default::default() - }; - let mut temporary_dir = None; - let src_imageref = if state.skopeo_supports_containers_storage { - // We always use exactly the digest of the running image to ensure predictability. - let digest = state - .source - .digest - .as_ref() - .ok_or_else(|| anyhow::anyhow!("Missing container image digest"))?; - let spec = crate::utils::digested_pullspec(&state.source.imageref.name, digest); - ostree_container::ImageReference { - transport: ostree_container::Transport::ContainerStorage, - name: spec, + let (src_imageref, proxy_cfg) = match &state.source.in_host_mountns { + None => (state.source.imageref.clone(), None), + Some(host_mountns_info) => { + let src_imageref = if host_mountns_info.skopeo_supports_containers_storage { + // We always use exactly the digest of the running image to ensure predictability. + let digest = state + .source + .digest + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Missing container image digest"))?; + let spec = crate::utils::digested_pullspec(&state.source.imageref.name, digest); + ostree_container::ImageReference { + transport: ostree_container::Transport::ContainerStorage, + name: spec, + } + } else { + let td = tempfile::tempdir_in("/var/tmp")?; + let path: &Utf8Path = td.path().try_into().unwrap(); + let r = copy_to_oci(&state.source.imageref, path)?; + temporary_dir = Some(td); + r + }; + + // We need to fetch the container image from the root mount namespace + let skopeo_cmd = run_in_host_mountns("skopeo"); + let proxy_cfg = ostree_container::store::ImageProxyConfig { + skopeo_cmd: Some(skopeo_cmd), + ..Default::default() + }; + + (src_imageref, Some(proxy_cfg)) } - } else { - let td = tempfile::tempdir_in("/var/tmp")?; - let path: &Utf8Path = td.path().try_into().unwrap(); - let r = copy_to_oci(&state.source.imageref, path)?; - temporary_dir = Some(td); - r }; let src_imageref = ostree_container::OstreeImageReference { // There are no signatures to verify since we're fetching the already @@ -605,7 +662,7 @@ async fn initialize_ostree_root_from_self( let mut options = ostree_container::deploy::DeployOpts::default(); options.kargs = Some(kargs.as_slice()); options.target_imgref = Some(&state.target_imgref); - options.proxy_cfg = Some(proxy_cfg); + options.proxy_cfg = proxy_cfg; println!("Creating initial deployment"); let target_image = state.target_imgref.to_string(); let state = @@ -910,6 +967,7 @@ async fn verify_target_fetch(imgref: &ostree_container::OstreeImageReference) -> /// Preparation for an install; validates and prepares some (thereafter immutable) global state. async fn prepare_install( config_opts: InstallConfigOpts, + source_opts: InstallSourceOpts, target_opts: InstallTargetOpts, ) -> Result> { // We need full root privileges, i.e. --privileged in podman @@ -923,16 +981,20 @@ async fn prepare_install( let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority()) .context("Opening /")?; - // This command currently *must* be run inside a privileged container. - let container_info = crate::containerenv::get_container_execution_info(&rootfs)?; - if let Some("1") = container_info.rootless.as_deref() { - anyhow::bail!("Cannot install from rootless podman; this command must be run as root"); - } - - let skopeo_supports_containers_storage = skopeo_supports_containers_storage() - .context("Failed to run skopeo (it currently must be installed in the host root)")?; + let source = match source_opts.source_imgref { + None => { + let container_info = crate::containerenv::get_container_execution_info(&rootfs)?; + // This command currently *must* be run inside a privileged container. + if let Some("1") = container_info.rootless.as_deref() { + anyhow::bail!( + "Cannot install from rootless podman; this command must be run as root" + ); + } - let source = SourceInfo::from_container(&container_info)?; + SourceInfo::from_container(&container_info)? + } + Some(source) => SourceInfo::from_imageref(&source)?, + }; // Parse the target CLI image reference options and create the *target* image // reference, which defaults to pulling from a registry. @@ -986,7 +1048,6 @@ async fn prepare_install( // combines our command line options along with some bind mounts from the host. let state = Arc::new(State { override_disable_selinux, - skopeo_supports_containers_storage, setenforce_guard, source, config_opts, @@ -1069,7 +1130,7 @@ pub(crate) async fn install_to_disk(opts: InstallToDiskOpts) -> Result<()> { anyhow::bail!("Not a block device: {}", block_opts.device); } } - let state = prepare_install(opts.config_opts, opts.target_opts).await?; + let state = prepare_install(opts.config_opts, opts.source_opts, opts.target_opts).await?; // This is all blocking stuff let mut rootfs = { @@ -1178,7 +1239,7 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu } // Gather global state, destructuring the provided options - let state = prepare_install(opts.config_opts, opts.target_opts).await?; + let state = prepare_install(opts.config_opts, opts.source_opts, opts.target_opts).await?; match fsopts.replace { Some(ReplaceMode::Wipe) => {