Skip to content

Commit

Permalink
Support RPMs installing in /opt and /usr/local
Browse files Browse the repository at this point in the history
This solves the `/opt` problem by using the new state overlay concept in
OSTree: an overlay filesystem is mounted on top of `/usr/lib/opt` and
the upper dir is automatically "rebased" whenever new content comes in.
Concretely, this means that app state is carried forward, all while
allowing the (OSTree-managed) package contents to be updated.

We also solve the `/usr/local` problem the same way. The app state issue
isn't really present there, but `/usr/local` has traditionally been
system state. We want to keep supporting dropping files there all while
also supporting shipping OSTree-owned content.

See also: ostreedev/ostree#3113
Fixes: #233
  • Loading branch information
jlebon authored and cgwalters committed Jan 30, 2024
1 parent 139b776 commit 0d7ac2d
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 6 deletions.
6 changes: 6 additions & 0 deletions docs/treefile.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,3 +479,9 @@ version of `rpm-ostree`.
names to use when substituting variables in yum repo files. The `releasever`
variable name is invalid. Use the `releasever` key instead. The `basearch`
name is invalid; it is filled in automatically.
* `opt-usrlocal-overlays`: boolean, optional: Defaults to `false`. By
default, `/opt` and `/usr/local` are symlinks to subdirectories in `/
var`. This prevents the ability to compose with packages that install in
those directories. If enabled, RPMs with `/opt` and `/usr/local` content
are allowed; client-side, both paths are writable overlay directories on.
Requires libostree v2023.9+.
82 changes: 76 additions & 6 deletions rust/src/composepost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,20 +145,32 @@ fn compose_init_rootfs_transient(rootfs_dfd: &cap_std::fs::Dir) -> Result<()> {
/// This is hardcoded; in the future we may make more things configurable,
/// but the goal is for all state to be in `/etc` and `/var`.
#[context("Initializing rootfs")]
fn compose_init_rootfs_strict(rootfs_dfd: &cap_std::fs::Dir, tmp_is_dir: bool) -> Result<()> {
fn compose_init_rootfs_strict(
rootfs_dfd: &cap_std::fs::Dir,
tmp_is_dir: bool,
opt_state_overlay: bool,
) -> Result<()> {
println!("Initializing rootfs");

compose_init_rootfs_base(rootfs_dfd, tmp_is_dir)?;

const OPT_SYMLINK_LEGACY: &str = "var/opt";
const OPT_SYMLINK_STATEOVERLAY: &str = "usr/lib/opt";
let opt_symlink = if opt_state_overlay {
OPT_SYMLINK_STATEOVERLAY
} else {
OPT_SYMLINK_LEGACY
};

// This is used in the case where we don't have a transient rootfs; redirect
// these toplevel directories underneath /var.
const OSTREE_STRICT_MODE_SYMLINKS: &[(&str, &str)] = &[
("var/opt", "opt"),
let ostree_strict_mode_symlinks: &[(&str, &str)] = &[
(opt_symlink, "opt"),
("var/srv", "srv"),
("var/mnt", "mnt"),
("run/media", "media"),
];
OSTREE_STRICT_MODE_SYMLINKS
ostree_strict_mode_symlinks
.par_iter()
.try_for_each(|&(dest, src)| {
rootfs_dfd
Expand Down Expand Up @@ -212,7 +224,15 @@ pub fn compose_prepare_rootfs(
return Ok(());
}

compose_init_rootfs_strict(target_rootfs_dfd, tmp_is_dir)?;
compose_init_rootfs_strict(
target_rootfs_dfd,
tmp_is_dir,
treefile
.parsed
.base
.opt_usrlocal_overlays
.unwrap_or_default(),
)?;

println!("Moving /usr to target");
src_rootfs_dfd.rename("usr", target_rootfs_dfd, "usr")?;
Expand Down Expand Up @@ -606,6 +626,32 @@ fn compose_postprocess_rpmdb(rootfs_dfd: &Dir) -> Result<()> {
Ok(())
}

/// Enables [email protected] for /usr/lib/opt and /usr/local. These
/// symlinks are also used later in the compose process (and client-side composes)
/// as a way to check that state overlays are turned on.
fn compose_postprocess_state_overlays(rootfs_dfd: &Dir) -> Result<()> {
let mut db = cap_std::fs::DirBuilder::new();
db.recursive(true);
db.mode(0o755);
let localfs_requires = Path::new("usr/lib/systemd/system/local-fs.target.requires");
rootfs_dfd.ensure_dir_with(localfs_requires, &db)?;

const UNITS: &[&str] = &[
"[email protected]",
"[email protected]",
];

UNITS.par_iter().try_for_each(|&unit| {
let target = Path::new("..").join(unit);
let linkpath = localfs_requires.join(unit);
rootfs_dfd
.symlink(target, linkpath)
.with_context(|| format!("Enabling {unit}"))
})?;

Ok(())
}

/// Rust portion of rpmostree_treefile_postprocessing()
pub fn compose_postprocess(
rootfs_dfd: i32,
Expand All @@ -627,6 +673,15 @@ pub fn compose_postprocess(
compose_postprocess_default_target(rootfs, t)?;
}

if treefile
.parsed
.base
.opt_usrlocal_overlays
.unwrap_or_default()
{
compose_postprocess_state_overlays(rootfs)?;
}

treefile.write_compose_json(rootfs)?;

let etc_guard = crate::core::prepare_tempetc_guard(rootfs_dfd.as_raw_fd())?;
Expand Down Expand Up @@ -955,6 +1010,17 @@ fn convert_path_to_tmpfiles_d_recurse(
Ok(())
}

fn state_overlay_enabled(rootfs_dfd: &cap_std::fs::Dir, state_overlay: &str) -> Result<bool> {
let linkname = format!(
"usr/lib/systemd/system/local-fs.target.requires/ostree-state-overlay@{state_overlay}.service"
);
match rootfs_dfd.symlink_metadata_optional(&linkname)? {
Some(meta) if meta.is_symlink() => Ok(true),
Some(_) => Err(anyhow!("{linkname} is not a symlink")),
None => Ok(false),
}
}

/// Walk over the root filesystem and perform some core conversions
/// from RPM conventions to OSTree conventions.
///
Expand All @@ -969,7 +1035,11 @@ pub fn rootfs_prepare_links(rootfs_dfd: i32, skip_usrlocal: bool) -> CxxResult<(
db.recursive(true);

if !skip_usrlocal {
if !crate::ostree_prepareroot::transient_root_enabled(rootfs)? {
if state_overlay_enabled(rootfs, "usr-local")? {
// because of the filesystem lua issue (see
// compose_init_rootfs_base()) we need to create this manually
rootfs.ensure_dir_with("usr/local", &db)?;
} else if !crate::ostree_prepareroot::transient_root_enabled(rootfs)? {
// Unconditionally drop /usr/local and replace it with a symlink.
rootfs
.remove_all_optional("usr/local")
Expand Down
3 changes: 3 additions & 0 deletions rust/src/treefile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ fn treefile_merge(dest: &mut TreeComposeConfig, src: &mut TreeComposeConfig) {
documentation,
boot_location,
tmp_is_dir,
opt_usrlocal_overlays,
default_target,
machineid_compat,
releasever,
Expand Down Expand Up @@ -2531,6 +2532,8 @@ pub(crate) struct BaseComposeConfigFields {
pub(crate) boot_location: Option<BootLocation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) tmp_is_dir: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) opt_usrlocal_overlays: Option<bool>,

// systemd
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down
45 changes: 45 additions & 0 deletions tests/compose/test-state-overlays.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/bin/bash
set -xeuo pipefail

dn=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=libcomposetest.sh
. "${dn}/libcomposetest.sh"

# Add a local rpm-md repo so we can mutate local test packages
treefile_append "repos" '["test-repo"]'

# An RPM that installs in /opt
build_rpm test-opt \
install "mkdir -p %{buildroot}/opt/megacorp/bin
install %{name} %{buildroot}/opt/megacorp/bin" \
files "/opt/megacorp"

# An RPM that installs in /usr/local
build_rpm test-usr-local \
install "mkdir -p %{buildroot}/usr/local/bin
install %{name} %{buildroot}/usr/local/bin" \
files "/usr/local/bin/%{name}"

echo gpgcheck=0 >> yumrepo.repo
ln "$PWD/yumrepo.repo" config/yumrepo.repo

# the top-level manifest doesn't have any packages, so just set it
treefile_append "packages" '["test-opt", "test-usr-local"]'

# enable state overlays
treefile_set "opt-usrlocal-overlays" 'True'

runcompose

# shellcheck disable=SC2154
ostree --repo="${repo}" ls -R "${treeref}" /usr/lib/opt > opt.txt
assert_file_has_content opt.txt "/usr/lib/opt/megacorp/bin/test-opt"

ostree --repo="${repo}" ls -R "${treeref}" /usr/local > usr-local.txt
assert_file_has_content usr-local.txt "/usr/local/bin/test-usr-local"

ostree --repo="${repo}" ls -R "${treeref}" /usr/lib/systemd/system/local-fs.target.requires > local-fs.txt
assert_file_has_content local-fs.txt "[email protected]"
assert_file_has_content local-fs.txt "[email protected]"

echo "ok /opt and /usr/local RPMs"

0 comments on commit 0d7ac2d

Please sign in to comment.