Skip to content

Commit 7c3ed0b

Browse files
committed
install: add --source-imgref
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 <[email protected]>
1 parent cad0718 commit 7c3ed0b

File tree

2 files changed

+116
-40
lines changed

2 files changed

+116
-40
lines changed

docs/install.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,18 @@ At the current time, leftover data in `/` is **NOT** automatically cleaned up.
234234
be useful, because it allows the new image to automatically import data from the previous
235235
host system! For example, things like SSH keys or container images can be copied
236236
and then deleted from the original.
237+
238+
### Using `bootc install to-filesystem --source-imgref <imgref>`
239+
240+
By default, `bootc install` has to be run inside a podman container. With this assumption,
241+
it can escape the container, find the source container image (including its layers) in
242+
the podman's container storage and use it to create the image.
243+
244+
When `--source-imgref <imgref>` is given, `bootc` no longer assumes that it runs inside podman.
245+
Instead, the given container image reference (see [containers-transports(5)](https://github.com/containers/image/blob/main/docs/containers-transports.5.md)
246+
for accepted formats) is used to fetch the image. Note that `bootc install` still has to be
247+
run inside a chroot created from the container image. However, this allows users to use
248+
a different sandboxing tool (e.g. [bubblewrap](https://github.com/containers/bubblewrap)).
249+
250+
This argument is mainly useful for 3rd-party tooling for building disk images from bootable
251+
containers (e.g. based on [osbuild](https://github.com/osbuild/osbuild)).

lib/src/install.rs

Lines changed: 101 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,18 @@ pub(crate) struct InstallTargetOpts {
102102
pub(crate) skip_fetch_check: bool,
103103
}
104104

105+
#[derive(clap::Args, Debug, Clone, Serialize, Deserialize)]
106+
pub(crate) struct InstallSourceOpts {
107+
/// Install the system from an explicitly given source.
108+
///
109+
/// By default, bootc install and install-to-filesystem assumes that it runs in a podman container, and
110+
/// it takes the container image to install from the podman's container registry.
111+
/// If --source-imgref is given, bootc uses it as the installation source, instead of the behaviour explained
112+
/// in the previous paragraph. See skopeo(1) for accepted formats.
113+
#[clap(long)]
114+
pub(crate) source_imgref: Option<String>,
115+
}
116+
105117
#[derive(clap::Args, Debug, Clone, Serialize, Deserialize)]
106118
pub(crate) struct InstallConfigOpts {
107119
/// Disable SELinux in the target (installed) system.
@@ -137,6 +149,10 @@ pub(crate) struct InstallToDiskOpts {
137149
#[serde(flatten)]
138150
pub(crate) block_opts: InstallBlockDeviceOpts,
139151

152+
#[clap(flatten)]
153+
#[serde(flatten)]
154+
pub(crate) source_opts: InstallSourceOpts,
155+
140156
#[clap(flatten)]
141157
#[serde(flatten)]
142158
pub(crate) target_opts: InstallTargetOpts,
@@ -209,6 +225,9 @@ pub(crate) struct InstallToFilesystemOpts {
209225
#[clap(flatten)]
210226
pub(crate) filesystem_opts: InstallTargetFilesystemOpts,
211227

228+
#[clap(flatten)]
229+
pub(crate) source_opts: InstallSourceOpts,
230+
212231
#[clap(flatten)]
213232
pub(crate) target_opts: InstallTargetOpts,
214233

@@ -225,15 +244,22 @@ pub(crate) struct SourceInfo {
225244
pub(crate) digest: Option<String>,
226245
/// Whether or not SELinux appears to be enabled in the source commit
227246
pub(crate) selinux: bool,
247+
/// Whether the source is available in the host mount namespace
248+
pub(crate) in_host_mountns: Option<HostMountnsInfo>,
249+
}
250+
251+
/// Information about the host mount namespace
252+
#[derive(Debug, Clone)]
253+
pub(crate) struct HostMountnsInfo {
254+
/// True if the skoepo on host supports containers-storage:
255+
pub(crate) skopeo_supports_containers_storage: bool,
228256
}
229257

230258
// Shared read-only global state
231259
pub(crate) struct State {
232260
pub(crate) source: SourceInfo,
233261
/// Force SELinux off in target system
234262
pub(crate) override_disable_selinux: bool,
235-
/// True if the skoepo on host supports containers-storage:
236-
pub(crate) skopeo_supports_containers_storage: bool,
237263
#[allow(dead_code)]
238264
pub(crate) setenforce_guard: Option<crate::lsm::SetEnforceGuard>,
239265
#[allow(dead_code)]
@@ -368,6 +394,29 @@ impl SourceInfo {
368394
name: container_info.image.clone(),
369395
};
370396
let digest = crate::podman::imageid_to_digest(&container_info.imageid)?;
397+
398+
let skopeo_supports_containers_storage = skopeo_supports_containers_storage()
399+
.context("Failed to run skopeo (it currently must be installed in the host root)")?;
400+
Self::from(
401+
imageref,
402+
Some(digest),
403+
Some(HostMountnsInfo {
404+
skopeo_supports_containers_storage,
405+
}),
406+
)
407+
}
408+
409+
#[context("Creating source info from a given imageref")]
410+
pub(crate) fn from_imageref(imageref: &str) -> Result<Self> {
411+
let imageref = ostree_container::ImageReference::try_from(imageref)?;
412+
Self::from(imageref, None, None)
413+
}
414+
415+
fn from(
416+
imageref: ostree_container::ImageReference,
417+
digest: Option<String>,
418+
in_host_mountns: Option<HostMountnsInfo>,
419+
) -> Result<Self> {
371420
let cancellable = ostree::gio::Cancellable::NONE;
372421
let commit = Task::new("Reading ostree commit", "ostree")
373422
.args(["--repo=/ostree/repo", "rev-parse", "--single"])
@@ -384,8 +433,9 @@ impl SourceInfo {
384433
let selinux = crate::lsm::xattrs_have_selinux(&xattrs);
385434
Ok(Self {
386435
imageref,
387-
digest: Some(digest),
436+
digest,
388437
selinux,
438+
in_host_mountns,
389439
})
390440
}
391441
}
@@ -562,32 +612,39 @@ async fn initialize_ostree_root_from_self(
562612
let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs)));
563613
sysroot.load(cancellable)?;
564614

565-
// We need to fetch the container image from the root mount namespace
566-
let skopeo_cmd = run_in_host_mountns("skopeo");
567-
let proxy_cfg = ostree_container::store::ImageProxyConfig {
568-
skopeo_cmd: Some(skopeo_cmd),
569-
..Default::default()
570-
};
571-
572615
let mut temporary_dir = None;
573-
let src_imageref = if state.skopeo_supports_containers_storage {
574-
// We always use exactly the digest of the running image to ensure predictability.
575-
let digest = state
576-
.source
577-
.digest
578-
.as_ref()
579-
.ok_or_else(|| anyhow::anyhow!("Missing container image digest"))?;
580-
let spec = crate::utils::digested_pullspec(&state.source.imageref.name, digest);
581-
ostree_container::ImageReference {
582-
transport: ostree_container::Transport::ContainerStorage,
583-
name: spec,
616+
let (src_imageref, proxy_cfg) = match &state.source.in_host_mountns {
617+
None => (state.source.imageref.clone(), None),
618+
Some(host_mountns_info) => {
619+
let src_imageref = if host_mountns_info.skopeo_supports_containers_storage {
620+
// We always use exactly the digest of the running image to ensure predictability.
621+
let digest = state
622+
.source
623+
.digest
624+
.as_ref()
625+
.ok_or_else(|| anyhow::anyhow!("Missing container image digest"))?;
626+
let spec = crate::utils::digested_pullspec(&state.source.imageref.name, digest);
627+
ostree_container::ImageReference {
628+
transport: ostree_container::Transport::ContainerStorage,
629+
name: spec,
630+
}
631+
} else {
632+
let td = tempfile::tempdir_in("/var/tmp")?;
633+
let path: &Utf8Path = td.path().try_into().unwrap();
634+
let r = copy_to_oci(&state.source.imageref, path)?;
635+
temporary_dir = Some(td);
636+
r
637+
};
638+
639+
// We need to fetch the container image from the root mount namespace
640+
let skopeo_cmd = run_in_host_mountns("skopeo");
641+
let proxy_cfg = ostree_container::store::ImageProxyConfig {
642+
skopeo_cmd: Some(skopeo_cmd),
643+
..Default::default()
644+
};
645+
646+
(src_imageref, Some(proxy_cfg))
584647
}
585-
} else {
586-
let td = tempfile::tempdir_in("/var/tmp")?;
587-
let path: &Utf8Path = td.path().try_into().unwrap();
588-
let r = copy_to_oci(&state.source.imageref, path)?;
589-
temporary_dir = Some(td);
590-
r
591648
};
592649
let src_imageref = ostree_container::OstreeImageReference {
593650
// There are no signatures to verify since we're fetching the already
@@ -605,7 +662,7 @@ async fn initialize_ostree_root_from_self(
605662
let mut options = ostree_container::deploy::DeployOpts::default();
606663
options.kargs = Some(kargs.as_slice());
607664
options.target_imgref = Some(&state.target_imgref);
608-
options.proxy_cfg = Some(proxy_cfg);
665+
options.proxy_cfg = proxy_cfg;
609666
println!("Creating initial deployment");
610667
let target_image = state.target_imgref.to_string();
611668
let state =
@@ -910,6 +967,7 @@ async fn verify_target_fetch(imgref: &ostree_container::OstreeImageReference) ->
910967
/// Preparation for an install; validates and prepares some (thereafter immutable) global state.
911968
async fn prepare_install(
912969
config_opts: InstallConfigOpts,
970+
source_opts: InstallSourceOpts,
913971
target_opts: InstallTargetOpts,
914972
) -> Result<Arc<State>> {
915973
// We need full root privileges, i.e. --privileged in podman
@@ -923,16 +981,20 @@ async fn prepare_install(
923981
let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())
924982
.context("Opening /")?;
925983

926-
// This command currently *must* be run inside a privileged container.
927-
let container_info = crate::containerenv::get_container_execution_info(&rootfs)?;
928-
if let Some("1") = container_info.rootless.as_deref() {
929-
anyhow::bail!("Cannot install from rootless podman; this command must be run as root");
930-
}
931-
932-
let skopeo_supports_containers_storage = skopeo_supports_containers_storage()
933-
.context("Failed to run skopeo (it currently must be installed in the host root)")?;
984+
let source = match source_opts.source_imgref {
985+
None => {
986+
let container_info = crate::containerenv::get_container_execution_info(&rootfs)?;
987+
// This command currently *must* be run inside a privileged container.
988+
if let Some("1") = container_info.rootless.as_deref() {
989+
anyhow::bail!(
990+
"Cannot install from rootless podman; this command must be run as root"
991+
);
992+
}
934993

935-
let source = SourceInfo::from_container(&container_info)?;
994+
SourceInfo::from_container(&container_info)?
995+
}
996+
Some(source) => SourceInfo::from_imageref(&source)?,
997+
};
936998

937999
// Parse the target CLI image reference options and create the *target* image
9381000
// reference, which defaults to pulling from a registry.
@@ -986,7 +1048,6 @@ async fn prepare_install(
9861048
// combines our command line options along with some bind mounts from the host.
9871049
let state = Arc::new(State {
9881050
override_disable_selinux,
989-
skopeo_supports_containers_storage,
9901051
setenforce_guard,
9911052
source,
9921053
config_opts,
@@ -1069,7 +1130,7 @@ pub(crate) async fn install_to_disk(opts: InstallToDiskOpts) -> Result<()> {
10691130
anyhow::bail!("Not a block device: {}", block_opts.device);
10701131
}
10711132
}
1072-
let state = prepare_install(opts.config_opts, opts.target_opts).await?;
1133+
let state = prepare_install(opts.config_opts, opts.source_opts, opts.target_opts).await?;
10731134

10741135
// This is all blocking stuff
10751136
let mut rootfs = {
@@ -1178,7 +1239,7 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu
11781239
}
11791240

11801241
// Gather global state, destructuring the provided options
1181-
let state = prepare_install(opts.config_opts, opts.target_opts).await?;
1242+
let state = prepare_install(opts.config_opts, opts.source_opts, opts.target_opts).await?;
11821243

11831244
match fsopts.replace {
11841245
Some(ReplaceMode::Wipe) => {

0 commit comments

Comments
 (0)