mirror of
https://github.com/containers/bootc.git
synced 2026-02-07 03:45:28 +01:00
I moved the code there; I plan to use open_dir_noxdev in the tmpfiles code too which can't depend on lib/util. Signed-off-by: Colin Walters <walters@verbum.org>
227 lines
8.0 KiB
Rust
227 lines
8.0 KiB
Rust
use std::future::Future;
|
|
use std::io::Write;
|
|
use std::os::fd::BorrowedFd;
|
|
use std::process::Command;
|
|
use std::time::Duration;
|
|
|
|
use anyhow::{Context, Result};
|
|
use bootc_utils::CommandRunExt;
|
|
use camino::Utf8Path;
|
|
use cap_std_ext::cap_std::fs::Dir;
|
|
use cap_std_ext::dirext::CapStdExtDirExt;
|
|
use cap_std_ext::prelude::CapStdExtCommandExt;
|
|
use fn_error_context::context;
|
|
use indicatif::HumanDuration;
|
|
use libsystemd::logging::journal_print;
|
|
use ostree::glib;
|
|
use ostree_ext::container::SignatureSource;
|
|
use ostree_ext::ostree;
|
|
|
|
/// Try to look for keys injected by e.g. rpm-ostree requesting machine-local
|
|
/// changes; if any are present, return `true`.
|
|
pub(crate) fn origin_has_rpmostree_stuff(kf: &glib::KeyFile) -> bool {
|
|
// These are groups set in https://github.com/coreos/rpm-ostree/blob/27f72dce4f9b5c176ad030911c12354e2498c07d/rust/src/origin.rs#L23
|
|
// TODO: Add some notion of "owner" into origin files
|
|
for group in ["rpmostree", "packages", "overrides", "modules"] {
|
|
if kf.has_group(group) {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
// Access the file descriptor for a sysroot
|
|
#[allow(unsafe_code)]
|
|
pub(crate) fn sysroot_fd(sysroot: &ostree::Sysroot) -> BorrowedFd {
|
|
unsafe { BorrowedFd::borrow_raw(sysroot.fd()) }
|
|
}
|
|
|
|
// Return a cap-std `Dir` type for a sysroot
|
|
pub(crate) fn sysroot_dir(sysroot: &ostree::Sysroot) -> Result<Dir> {
|
|
Dir::reopen_dir(&sysroot_fd(sysroot)).map_err(Into::into)
|
|
}
|
|
|
|
// Return a cap-std `Dir` type for a deployment.
|
|
// TODO: in the future this should perhaps actually mount via composefs
|
|
pub(crate) fn deployment_fd(
|
|
sysroot: &ostree::Sysroot,
|
|
deployment: &ostree::Deployment,
|
|
) -> Result<Dir> {
|
|
let sysroot_dir = &Dir::reopen_dir(&sysroot_fd(sysroot))?;
|
|
let dirpath = sysroot.deployment_dirpath(deployment);
|
|
sysroot_dir.open_dir(&dirpath).map_err(Into::into)
|
|
}
|
|
|
|
/// Given an mount option string list like foo,bar=baz,something=else,ro parse it and find
|
|
/// the first entry like $optname=
|
|
/// This will not match a bare `optname` without an equals.
|
|
pub(crate) fn find_mount_option<'a>(
|
|
option_string_list: &'a str,
|
|
optname: &'_ str,
|
|
) -> Option<&'a str> {
|
|
option_string_list
|
|
.split(',')
|
|
.filter_map(|k| k.split_once('='))
|
|
.filter_map(|(k, v)| (k == optname).then_some(v))
|
|
.next()
|
|
}
|
|
|
|
/// Given a target directory, if it's a read-only mount, then remount it writable
|
|
#[context("Opening {target} with writable mount")]
|
|
pub(crate) fn open_dir_remount_rw(root: &Dir, target: &Utf8Path) -> Result<Dir> {
|
|
if matches!(root.is_mountpoint(target), Ok(Some(true))) {
|
|
tracing::debug!("Target {target} is a mountpoint, remounting rw");
|
|
let st = Command::new("mount")
|
|
.args(["-o", "remount,rw", target.as_str()])
|
|
.cwd_dir(root.try_clone()?)
|
|
.status()?;
|
|
|
|
anyhow::ensure!(st.success(), "Failed to remount: {st:?}");
|
|
}
|
|
root.open_dir(target).map_err(anyhow::Error::new)
|
|
}
|
|
|
|
/// Given a target path, remove its immutability if present
|
|
#[context("Removing immutable flag from {target}")]
|
|
pub(crate) fn remove_immutability(root: &Dir, target: &Utf8Path) -> Result<()> {
|
|
use anyhow::ensure;
|
|
|
|
tracing::debug!("Target {target} is a mountpoint, remounting rw");
|
|
let st = Command::new("chattr")
|
|
.args(["-i", target.as_str()])
|
|
.cwd_dir(root.try_clone()?)
|
|
.status()?;
|
|
|
|
ensure!(st.success(), "Failed to remove immutability: {st:?}");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn spawn_editor(tmpf: &tempfile::NamedTempFile) -> Result<()> {
|
|
let editor_variables = ["EDITOR"];
|
|
// These roughly match https://github.com/systemd/systemd/blob/769ca9ab557b19ee9fb5c5106995506cace4c68f/src/shared/edit-util.c#L275
|
|
let backup_editors = ["nano", "vim", "vi"];
|
|
let editor = editor_variables.into_iter().find_map(std::env::var_os);
|
|
let editor = if let Some(e) = editor.as_ref() {
|
|
e.to_str()
|
|
} else {
|
|
backup_editors
|
|
.into_iter()
|
|
.find(|v| std::path::Path::new("/usr/bin").join(v).exists())
|
|
};
|
|
let editor =
|
|
editor.ok_or_else(|| anyhow::anyhow!("$EDITOR is unset, and no backup editor found"))?;
|
|
let mut editor_args = editor.split_ascii_whitespace();
|
|
let argv0 = editor_args
|
|
.next()
|
|
.ok_or_else(|| anyhow::anyhow!("Invalid editor: {editor}"))?;
|
|
Command::new(argv0)
|
|
.args(editor_args)
|
|
.arg(tmpf.path())
|
|
.lifecycle_bind()
|
|
.run()
|
|
.with_context(|| format!("Invoking editor {editor} failed"))
|
|
}
|
|
|
|
/// Convert a combination of values (likely from CLI parsing) into a signature source
|
|
pub(crate) fn sigpolicy_from_opt(enforce_container_verification: bool) -> SignatureSource {
|
|
match enforce_container_verification {
|
|
true => SignatureSource::ContainerPolicy,
|
|
false => SignatureSource::ContainerPolicyAllowInsecure,
|
|
}
|
|
}
|
|
|
|
/// Output a warning message that we want to be quite visible.
|
|
/// The process (thread) execution will be delayed for a short time.
|
|
pub(crate) fn medium_visibility_warning(s: &str) {
|
|
anstream::eprintln!(
|
|
"{}{s}{}",
|
|
anstyle::AnsiColor::Red.render_fg(),
|
|
anstyle::Reset.render()
|
|
);
|
|
// When warning, add a sleep to ensure it's seen
|
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
|
}
|
|
|
|
/// Call an async task function, and write a message to stdout
|
|
/// with an automatic spinner to show that we're not blocked.
|
|
/// Note that generally the called function should not output
|
|
/// anything to stdout as this will interfere with the spinner.
|
|
pub(crate) async fn async_task_with_spinner<F, T>(msg: &str, f: F) -> T
|
|
where
|
|
F: Future<Output = T>,
|
|
{
|
|
let start_time = std::time::Instant::now();
|
|
let pb = indicatif::ProgressBar::new_spinner();
|
|
let style = indicatif::ProgressStyle::default_bar();
|
|
pb.set_style(style.template("{spinner} {msg}").unwrap());
|
|
pb.set_message(msg.to_string());
|
|
pb.enable_steady_tick(Duration::from_millis(150));
|
|
// We need to handle the case where we aren't connected to
|
|
// a tty, so indicatif would show nothing by default.
|
|
if pb.is_hidden() {
|
|
print!("{}...", msg);
|
|
std::io::stdout().flush().unwrap();
|
|
}
|
|
let r = f.await;
|
|
let elapsed = HumanDuration(start_time.elapsed());
|
|
let _ = journal_print(
|
|
libsystemd::logging::Priority::Info,
|
|
&format!("completed task in {elapsed}: {msg}"),
|
|
);
|
|
if pb.is_hidden() {
|
|
println!("done ({elapsed})");
|
|
} else {
|
|
pb.finish_with_message(format!("{msg}: done ({elapsed})"));
|
|
}
|
|
r
|
|
}
|
|
|
|
/// Given a possibly tagged image like quay.io/foo/bar:latest and a digest 0ab32..., return
|
|
/// the digested form quay.io/foo/bar:latest@sha256:0ab32...
|
|
/// If the image already has a digest, it will be replaced.
|
|
#[allow(dead_code)]
|
|
pub(crate) fn digested_pullspec(image: &str, digest: &str) -> String {
|
|
let image = image.rsplit_once('@').map(|v| v.0).unwrap_or(image);
|
|
format!("{image}@{digest}")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_digested_pullspec() {
|
|
let digest = "ebe3bdccc041864e5a485f1e755e242535c3b83d110c0357fe57f110b73b143e";
|
|
assert_eq!(
|
|
digested_pullspec("quay.io/example/foo:bar", digest),
|
|
format!("quay.io/example/foo:bar@{digest}")
|
|
);
|
|
assert_eq!(
|
|
digested_pullspec("quay.io/example/foo@sha256:otherdigest", digest),
|
|
format!("quay.io/example/foo@{digest}")
|
|
);
|
|
assert_eq!(
|
|
digested_pullspec("quay.io/example/foo", digest),
|
|
format!("quay.io/example/foo@{digest}")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_mount_option() {
|
|
const V1: &str = "rw,relatime,compress=foo,subvol=blah,fast";
|
|
assert_eq!(find_mount_option(V1, "subvol").unwrap(), "blah");
|
|
assert_eq!(find_mount_option(V1, "rw"), None);
|
|
assert_eq!(find_mount_option(V1, "somethingelse"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sigpolicy_from_opts() {
|
|
assert_eq!(sigpolicy_from_opt(true), SignatureSource::ContainerPolicy);
|
|
assert_eq!(
|
|
sigpolicy_from_opt(false),
|
|
SignatureSource::ContainerPolicyAllowInsecure
|
|
);
|
|
}
|
|
}
|