Skip to content

Commit

Permalink
install: add --source
Browse files Browse the repository at this point in the history
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 argument.
--source accepts a container image reference (the same one that skopeo uses).
When --source 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 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. 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 <ondrej@budai.cz>
  • Loading branch information
ondrejbudai committed Jan 16, 2024
1 parent 8122dd5 commit c96d517
Showing 1 changed file with 85 additions and 35 deletions.
120 changes: 85 additions & 35 deletions lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 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, hide = true)]
pub(crate) source: Option<String>,
}

#[derive(clap::Args, Debug, Clone, Serialize, Deserialize)]
pub(crate) struct InstallConfigOpts {
/// Disable SELinux in the target (installed) system.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,

Expand All @@ -225,15 +244,22 @@ pub(crate) struct SourceInfo {
pub(crate) digest: String,
/// 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<HostMountnsInfo>,
}

/// 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
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<crate::lsm::SetEnforceGuard>,
#[allow(dead_code)]
Expand Down Expand Up @@ -368,6 +394,20 @@ 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, digest, Some(HostMountnsInfo{skopeo_supports_containers_storage}))
}

#[context("Gathering source info from an external source")]
pub(crate) fn from_imageref(imageref: &str) -> Result<Self> {
let digest = crate::skopeo::oci_archive_digest(imageref)?;
let imageref = ostree_container::ImageReference::try_from(imageref)?;

Self::from(imageref, digest, None)
}

fn from(imageref: ostree_container::ImageReference, digest: String, in_host_mountns: Option<HostMountnsInfo>) -> Result<Self> {
let cancellable = ostree::gio::Cancellable::NONE;
let commit = Task::new("Reading ostree commit", "ostree")
.args(["--repo=/ostree/repo", "rev-parse", "--single"])
Expand All @@ -386,6 +426,7 @@ impl SourceInfo {
imageref,
digest,
selinux,
in_host_mountns,
})
}
}
Expand Down Expand Up @@ -562,28 +603,35 @@ 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 spec =
crate::utils::digested_pullspec(&state.source.imageref.name, &state.source.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 spec =
crate::utils::digested_pullspec(&state.source.imageref.name, &state.source.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
Expand All @@ -601,7 +649,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 =
Expand Down Expand Up @@ -906,6 +954,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<Arc<State>> {
// We need full root privileges, i.e. --privileged in podman
Expand All @@ -919,16 +968,18 @@ 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 {
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.
Expand Down Expand Up @@ -982,7 +1033,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,
Expand Down Expand Up @@ -1065,7 +1115,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 = {
Expand Down Expand Up @@ -1174,7 +1224,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) => {
Expand Down

0 comments on commit c96d517

Please sign in to comment.