Skip to content

Commit 0910876

Browse files
authored
Merge pull request #263 from ondrejbudai/source-arg
install: add --source-imgref
2 parents b75b647 + 7c3ed0b commit 0910876

File tree

3 files changed

+117
-37
lines changed

3 files changed

+117
-37
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/blockdev.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ pub(crate) fn find_parent_devices(device: &str) -> Result<Vec<String>> {
197197
let kind = dev
198198
.get("TYPE")
199199
.with_context(|| format!("device in hierarchy of {device} missing TYPE"))?;
200-
if kind == "disk" {
200+
if kind == "disk" || kind == "loop" {
201201
parents.push(name.clone());
202202
} else if kind == "mpath" {
203203
parents.push(name.clone());

lib/src/install.rs

Lines changed: 101 additions & 36 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

@@ -222,18 +241,25 @@ pub(crate) struct SourceInfo {
222241
/// Image reference we'll pull from (today always containers-storage: type)
223242
pub(crate) imageref: ostree_container::ImageReference,
224243
/// The digest to use for pulls
225-
pub(crate) digest: String,
244+
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"])
@@ -386,6 +435,7 @@ impl SourceInfo {
386435
imageref,
387436
digest,
388437
selinux,
438+
in_host_mountns,
389439
})
390440
}
391441
}
@@ -562,28 +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 spec =
576-
crate::utils::digested_pullspec(&state.source.imageref.name, &state.source.digest);
577-
ostree_container::ImageReference {
578-
transport: ostree_container::Transport::ContainerStorage,
579-
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))
580647
}
581-
} else {
582-
let td = tempfile::tempdir_in("/var/tmp")?;
583-
let path: &Utf8Path = td.path().try_into().unwrap();
584-
let r = copy_to_oci(&state.source.imageref, path)?;
585-
temporary_dir = Some(td);
586-
r
587648
};
588649
let src_imageref = ostree_container::OstreeImageReference {
589650
// There are no signatures to verify since we're fetching the already
@@ -601,7 +662,7 @@ async fn initialize_ostree_root_from_self(
601662
let mut options = ostree_container::deploy::DeployOpts::default();
602663
options.kargs = Some(kargs.as_slice());
603664
options.target_imgref = Some(&state.target_imgref);
604-
options.proxy_cfg = Some(proxy_cfg);
665+
options.proxy_cfg = proxy_cfg;
605666
println!("Creating initial deployment");
606667
let target_image = state.target_imgref.to_string();
607668
let state =
@@ -906,6 +967,7 @@ async fn verify_target_fetch(imgref: &ostree_container::OstreeImageReference) ->
906967
/// Preparation for an install; validates and prepares some (thereafter immutable) global state.
907968
async fn prepare_install(
908969
config_opts: InstallConfigOpts,
970+
source_opts: InstallSourceOpts,
909971
target_opts: InstallTargetOpts,
910972
) -> Result<Arc<State>> {
911973
// We need full root privileges, i.e. --privileged in podman
@@ -919,16 +981,20 @@ async fn prepare_install(
919981
let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())
920982
.context("Opening /")?;
921983

922-
// This command currently *must* be run inside a privileged container.
923-
let container_info = crate::containerenv::get_container_execution_info(&rootfs)?;
924-
if let Some("1") = container_info.rootless.as_deref() {
925-
anyhow::bail!("Cannot install from rootless podman; this command must be run as root");
926-
}
927-
928-
let skopeo_supports_containers_storage = skopeo_supports_containers_storage()
929-
.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+
}
930993

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

933999
// Parse the target CLI image reference options and create the *target* image
9341000
// reference, which defaults to pulling from a registry.
@@ -982,7 +1048,6 @@ async fn prepare_install(
9821048
// combines our command line options along with some bind mounts from the host.
9831049
let state = Arc::new(State {
9841050
override_disable_selinux,
985-
skopeo_supports_containers_storage,
9861051
setenforce_guard,
9871052
source,
9881053
config_opts,
@@ -1065,7 +1130,7 @@ pub(crate) async fn install_to_disk(opts: InstallToDiskOpts) -> Result<()> {
10651130
anyhow::bail!("Not a block device: {}", block_opts.device);
10661131
}
10671132
}
1068-
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?;
10691134

10701135
// This is all blocking stuff
10711136
let mut rootfs = {
@@ -1174,7 +1239,7 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu
11741239
}
11751240

11761241
// Gather global state, destructuring the provided options
1177-
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?;
11781243

11791244
match fsopts.replace {
11801245
Some(ReplaceMode::Wipe) => {

0 commit comments

Comments
 (0)