1
0
mirror of https://github.com/projectatomic/rpm-ostree.git synced 2026-02-05 09:45:27 +01:00

compose-rootfs: Add --source-root-rw

This is a hacky workaround for two distinct bugs:

- Needing /usr/share/rpm (because that's what rpm-ostree wants)
  in the repos container
- gpgkey= not working with --source-root (see below)

Closes: https://github.com/coreos/rpm-ostree/issues/5285

It's called `--source-root-rw` because the way we expect this
to work is in a Dockerfile, the `rw` flag will be set in `RUN --mount=type=bind,rw`
to make a temporary overlayfs, so we can go ahead and mutate that
root with our workarounds.

Signed-off-by: Colin Walters <walters@verbum.org>
This commit is contained in:
Colin Walters
2025-02-07 16:29:20 -05:00
parent 4f19575c04
commit 1c6fae8e77
2 changed files with 162 additions and 8 deletions

View File

@@ -4,13 +4,14 @@
use std::ffi::OsStr;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::os::fd::{AsFd, AsRawFd};
use std::process::Command;
use anyhow::{anyhow, Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use cap_std::fs::{Dir, MetadataExt};
use cap_std_ext::dirext::CapStdExtDirExt;
use clap::Parser;
use fn_error_context::context;
use oci_spec::image::ImageManifest;
@@ -26,6 +27,7 @@ use crate::cmdutils::CommandRunExt;
use crate::containers_storage::Mount;
use crate::cxxrsutil::{CxxResult, FFIGObjectWrapper};
use crate::isolation::self_command;
use crate::{RPMOSTREE_RPMDB_LOCATION, RPMOSTREE_SYSIMAGE_RPMDB};
const SYSROOT: &str = "sysroot";
const USR: &str = "usr";
@@ -94,7 +96,8 @@ struct ComposeImageOpts {
#[clap(long)]
#[clap(value_parser)]
/// Rootfs to use for resolving releasever if unset
/// Rootfs to use for resolving package system configuration, such
/// as the yum repository configuration, releasever, etc.
source_root: Option<Utf8PathBuf>,
/// Container authentication file
@@ -202,9 +205,16 @@ pub(crate) struct RootfsOpts {
ostree_repo: Option<Utf8PathBuf>,
/// Source root for package system configuration.
#[clap(long, value_parser)]
#[clap(long, value_parser, conflicts_with = "source_root_rw")]
source_root: Option<Utf8PathBuf>,
#[clap(long, value_parser, conflicts_with = "source_root")]
/// Rootfs to use for resolving package system configuration, such
/// as the yum repository configuration, releasever, etc.
///
/// The source root may be mutated to work around bugs.
source_root_rw: Option<Utf8PathBuf>,
/// Path to the input manifest
manifest: Utf8PathBuf,
@@ -312,14 +322,118 @@ impl BuildChunkedOCIOpts {
}
}
/// Given a .repo file, rewrite all references to gpgkey=file:// inside
/// it to point into the source_root (if the key can be found there).
/// This is a workaround for https://github.com/coreos/rpm-ostree/issues/5285
fn mutate_one_dnf_repo(
exec_root: &Dir,
source_root: &Utf8Path,
reposdir: &Dir,
name: &Utf8Path,
) -> Result<()> {
let r = reposdir.open(name).map(BufReader::new)?;
let mut w = Vec::new();
let mut modified = false;
for line in r.lines() {
let line = line?;
// Extract the value of gpgkey=, if it doesn't match then
// pass through the line.
let Some(value) = line
.split_once('=')
.filter(|kv| kv.0 == "gpgkey")
.map(|kv| kv.1)
else {
writeln!(w, "{line}")?;
continue;
};
// If the gpg key isn't a local file, pass through the line.
let Some(relpath) = value
.strip_prefix("file://")
.and_then(|path| path.strip_prefix('/'))
else {
writeln!(w, "{line}")?;
continue;
};
// If it doesn't exist in the source root, then assume the absolute
// reference is intentional.
let target_repo_file = source_root.join(relpath);
if !exec_root.try_exists(&target_repo_file)? {
writeln!(w, "{line}")?;
continue;
}
modified = true;
writeln!(w, "gpgkey=file:///{target_repo_file}")?;
}
if modified {
reposdir.write(name, w)?;
}
Ok(())
}
#[context("Preparing source root")]
fn mutate_source_root(exec_root: &Dir, source_root: &Utf8Path) -> Result<()> {
let source_root_dir = exec_root
.open_dir(source_root)
.with_context(|| format!("Opening {source_root}"))?;
if source_root_dir
.symlink_metadata_optional(RPMOSTREE_RPMDB_LOCATION)?
.is_none()
&& source_root_dir
.symlink_metadata_optional(RPMOSTREE_SYSIMAGE_RPMDB)?
.is_some()
{
source_root_dir
.symlink_contents("../lib/sysimage/rpm", RPMOSTREE_RPMDB_LOCATION)
.context("Symlinking rpmdb")?;
println!("Symlinked {RPMOSTREE_RPMDB_LOCATION} in source root");
}
if !source_root_dir.try_exists("etc")? {
return Ok(());
}
if let Some(repos) = source_root_dir
.open_dir_optional("etc/yum.repos.d")
.context("Opening yum.repos.d")?
{
for ent in repos.entries_utf8()? {
let ent = ent?;
if !ent.file_type()?.is_file() {
continue;
}
let name: Utf8PathBuf = ent.file_name()?.into();
let Some("repo") = name.extension() else {
continue;
};
mutate_one_dnf_repo(exec_root, source_root, &repos, &name)?;
}
}
Ok(())
}
impl RootfsOpts {
pub(crate) fn run(self) -> Result<()> {
pub(crate) fn run(mut self) -> Result<()> {
let manifest = self.manifest.as_path();
if self.dest.try_exists()? {
anyhow::bail!("Refusing to operate on extant target {}", self.dest);
}
// If we were passed a mutable source root, then let's work around some bugs
if let Some(rw) = self.source_root_rw.take() {
// Clap ensures this
assert!(self.source_root.is_none());
let exec_root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
// We could handle relative paths, but it's easier to require absolute.
// The mutation work below all happens via cap-std because it's way easier
// to unit test with that.
let Some(rw_relpath) = rw.strip_prefix("/").ok() else {
anyhow::bail!("Expected absolute path for source-root: {rw}");
};
mutate_source_root(&exec_root, rw_relpath)?;
self.source_root = Some(rw);
}
// Create a temporary directory for things
let td = tempfile::tempdir_in("/var/tmp")?;
let td_path: Utf8PathBuf = td.path().to_owned().try_into()?;
@@ -1029,4 +1143,46 @@ mod tests {
Ok(())
}
#[test]
fn test_mutate_source_root() -> Result<()> {
let rootfs = &cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
// Should be a no-op in an empty root
rootfs.create_dir("repos")?;
mutate_source_root(rootfs, "repos".into()).unwrap();
rootfs.create_dir_all("repos/usr/share")?;
rootfs.create_dir_all("repos/usr/lib/sysimage/rpm")?;
mutate_source_root(rootfs, "repos".into()).unwrap();
assert!(rootfs
.symlink_metadata("repos/usr/share/rpm")
.unwrap()
.is_symlink());
rootfs.create_dir_all("repos/etc/yum.repos.d")?;
rootfs.create_dir_all("repos/etc/pki/rpm-gpg")?;
rootfs.write("repos/etc/pki/rpm-gpg/repo2.key", "repo2 gpg key")?;
let orig_repo_content = indoc::indoc! { r#"
[repo]
baseurl=blah
gpgkey=https://example.com
[repo2]
baseurl=other
gpgkey=file:///etc/pki/rpm-gpg/repo2.key
[repo3]
baseurl=blah
gpgkey=file:///absolute/path/not-in-source-root
"#};
rootfs.write("repos/etc/yum.repos.d/test.repo", orig_repo_content)?;
mutate_source_root(rootfs, "repos".into()).unwrap();
let found_repos = rootfs.read_to_string("repos/etc/yum.repos.d/test.repo")?;
let expected = orig_repo_content.replace(
"gpgkey=file:///etc/pki/rpm-gpg/repo2.key",
"gpgkey=file:///repos/etc/pki/rpm-gpg/repo2.key",
);
similar_asserts::assert_eq!(expected, found_repos);
Ok(())
}
}

View File

@@ -15,11 +15,9 @@ EORUN
# Copy in our source code.
COPY . /src
WORKDIR /src
RUN --mount=type=bind,from=repos,src=/,dst=/repos <<EORUN
RUN --mount=type=bind,from=repos,src=/,dst=/repos,rw <<EORUN
set -xeuo pipefail
# Workaround for https://github.com/coreos/rpm-ostree/issues/5285
cp -a /repos/etc/pki/rpm-gpg/* /etc/pki/rpm-gpg
exec rpm-ostree experimental compose rootfs --source-root=/repos manifest.yaml /target-rootfs
exec rpm-ostree experimental compose rootfs --source-root-rw=/repos manifest.yaml /target-rootfs
EORUN
# This pulls in the rootfs generated in the previous step