-
Notifications
You must be signed in to change notification settings - Fork 198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support RPMs installing in /opt
and /usr/local
#4728
Changes from all commits
d7e2975
4d941ee
dd6ca8a
7fd72dc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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")?; | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Huh, doesn't cap-std barf on this? I did this PR a while ago which we use in other places; though maybe it doesn't actually check for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, seems to work here! :) |
||
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, | ||
|
@@ -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())?; | ||
|
@@ -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> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function in theory could live in ostree's rust code or ostree-ext, not that it matters much in practice. (Please don't move it just for this, but just a thought) |
||
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. | ||
/// | ||
|
@@ -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") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -392,7 +392,7 @@ fn path_is_ostree_compliant(path: &str) -> bool { | |
return true; | ||
} | ||
|
||
if path.starts_with("/usr/") && !path.starts_with("/usr/local") { | ||
if path.starts_with("/usr/") { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like this change needs to be gated on us detecting that a state overlay is enabled? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we want to be strict, yes. I didn't bother with this because Can pipe through the needed flag to retain current behaviour if wanted. |
||
return true; | ||
} | ||
|
||
|
@@ -491,7 +491,7 @@ mod tests { | |
assert_eq!(path_is_ostree_compliant(entry), false, "{}", entry); | ||
} | ||
|
||
let denied_cases = &["/var", "/etc", "/var/run", "/usr/local", "", "./", "usr/"]; | ||
let denied_cases = &["/var", "/etc", "/var/run", "", "./", "usr/"]; | ||
for entry in denied_cases { | ||
assert_eq!(path_is_ostree_compliant(entry), false, "{}", entry); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Traditionally, /usr/local has been a link to /var/usrlocal and /opt to /var/opt. | ||
# A new model now is to allow OSTree commit content in those directories. For | ||
# backwards compatibility, we keep the /var paths but flip the symlinks around. | ||
L /var/usrlocal - - - - ../usr/local | ||
L /var/opt - - - - ../usr/lib/opt |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,5 @@ | ||
# Traditionally, /usr/local has been a link to /var/usrlocal and /opt to /var/opt. | ||
# A new model now is to allow OSTree commit content in those directories. But | ||
# this dropin implements the old model. | ||
d /var/opt 0755 root root - | ||
d /var/usrlocal 0755 root root - |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4062,6 +4062,55 @@ rpmostree_context_assemble (RpmOstreeContext *self, GCancellable *cancellable, G | |
if (!build_rootfs_usrlinks (self, error)) | ||
return FALSE; | ||
|
||
/* This is purely for making it easier for people to test out the | ||
* state-overlay stuff until it's stabilized and part of base composes. */ | ||
if (g_getenv ("RPMOSTREE_EXPERIMENTAL_FORCE_OPT_USRLOCAL_OVERLAY")) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't it be better to document how to do this in a derived container build? The ergonomics around providing an environment variable in this context seems...difficult. |
||
{ | ||
rpmostree_output_message ( | ||
"Enabling experimental state overlay support for /opt and /usr/local"); | ||
|
||
struct stat stbuf; | ||
|
||
if (!glnx_fstatat_allow_noent (tmprootfs_dfd, "opt", &stbuf, AT_SYMLINK_NOFOLLOW, error)) | ||
return FALSE; | ||
if (errno == ENOENT || (errno == 0 && S_ISLNK (stbuf.st_mode))) | ||
{ | ||
if (errno == 0 && !glnx_unlinkat (tmprootfs_dfd, "opt", 0, error)) | ||
return FALSE; | ||
if (symlinkat ("usr/lib/opt", tmprootfs_dfd, "opt") < 0) | ||
return glnx_throw_errno_prefix (error, "symlinkat(/opt)"); | ||
} | ||
|
||
if (!glnx_fstatat_allow_noent (tmprootfs_dfd, "usr/local", &stbuf, AT_SYMLINK_NOFOLLOW, | ||
error)) | ||
return FALSE; | ||
if (errno == ENOENT || (errno == 0 && S_ISLNK (stbuf.st_mode))) | ||
{ | ||
if (errno == 0 && !glnx_unlinkat (tmprootfs_dfd, "usr/local", 0, error)) | ||
return FALSE; | ||
if (mkdirat (tmprootfs_dfd, "usr/local", 0755) < 0) | ||
return glnx_throw_errno_prefix (error, "mkdirat(/usr/local)"); | ||
} | ||
|
||
if (!glnx_shutil_mkdir_p_at (tmprootfs_dfd, "usr/lib/systemd/system/local-fs.target.wants", | ||
0755, cancellable, error)) | ||
return FALSE; | ||
|
||
if (symlinkat ("[email protected]", tmprootfs_dfd, | ||
"usr/lib/systemd/system/local-fs.target.wants/" | ||
"[email protected]") | ||
< 0 | ||
&& errno != EEXIST) | ||
return glnx_throw_errno_prefix (error, "enabling ostree-state-overlay for /usr/lib/opt"); | ||
|
||
if (symlinkat ( | ||
"[email protected]", tmprootfs_dfd, | ||
"usr/lib/systemd/system/local-fs.target.wants/[email protected]") | ||
< 0 | ||
&& errno != EEXIST) | ||
return glnx_throw_errno_prefix (error, "enabling ostree-state-overlay for /usr/local"); | ||
} | ||
|
||
/* We need up to date labels; the set of things needing relabeling | ||
* will have been calculated in sort_packages() | ||
*/ | ||
|
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" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one could probably live as a
const
given it's referenced twice.