Skip to content

Commit

Permalink
Merge pull request containers#741 from cgwalters/init-container-stora…
Browse files Browse the repository at this point in the history
…ge-prep3

A few prep commits for containers#724
  • Loading branch information
cgwalters authored Jul 30, 2024
2 parents e25e928 + 81556a4 commit 7520fa4
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 13 deletions.
3 changes: 2 additions & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ license = "MIT OR Apache-2.0"
repository = "https://github.com/containers/bootc"
readme = "README.md"
publish = false
rust-version = "1.63.0"
# For now don't bump this above what is currently shipped in RHEL9.
rust-version = "1.75.0"
default-run = "bootc"

# See https://github.com/coreos/cargo-vendor-filterer
Expand Down
4 changes: 3 additions & 1 deletion lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ name = "bootc-lib"
readme = "README.md"
repository = "https://github.com/containers/bootc"
version = "0.1.14"
# For now don't bump this above what is currently shipped in RHEL9;
# also keep in sync with the version in cli.
rust-version = "1.75.0"
build = "build.rs"

Expand All @@ -30,7 +32,7 @@ liboverdrop = "0.1.0"
libsystemd = "0.7"
openssl = "^0.10.64"
regex = "1.10.4"
rustix = { "version" = "0.38.34", features = ["thread", "fs", "system", "process"] }
rustix = { "version" = "0.38.34", features = ["thread", "fs", "system", "process", "mount"] }
schemars = { version = "0.8.17", features = ["chrono"] }
serde = { workspace = true, features = ["derive"] }
serde_ignored = "0.1.10"
Expand Down
121 changes: 110 additions & 11 deletions lib/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::future::Future;
use std::io::Write;
use std::io::{Read, Seek, Write};
use std::os::fd::BorrowedFd;
use std::process::Command;
use std::time::Duration;
Expand All @@ -15,17 +15,79 @@ pub(crate) trait CommandRunExt {
fn run(&mut self) -> Result<()>;
}

/// Helpers intended for [`std::process::ExitStatus`].
pub(crate) trait ExitStatusExt {
/// If the exit status signals it was not successful, return an error.
/// Note that we intentionally *don't* include the command string
/// in the output; we leave it to the caller to add that if they want,
/// as it may be verbose.
fn check_status(&mut self, stderr: std::fs::File) -> Result<()>;
}

/// Parse the last chunk (e.g. 1024 bytes) from the provided file,
/// ensure it's UTF-8, and return that value. This function is infallible;
/// if the file cannot be read for some reason, a copy of a static string
/// is returned.
fn last_utf8_content_from_file(mut f: std::fs::File) -> String {
// u16 since we truncate to just the trailing bytes here
// to avoid pathological error messages
const MAX_STDERR_BYTES: u16 = 1024;
let size = f
.metadata()
.map_err(|e| {
tracing::warn!("failed to fstat: {e}");
})
.map(|m| m.len().try_into().unwrap_or(u16::MAX))
.unwrap_or(0);
let size = size.min(MAX_STDERR_BYTES);
let seek_offset = -(size as i32);
let mut stderr_buf = Vec::with_capacity(size.into());
// We should never fail to seek()+read() really, but let's be conservative
let r = match f
.seek(std::io::SeekFrom::End(seek_offset.into()))
.and_then(|_| f.read_to_end(&mut stderr_buf))
{
Ok(_) => String::from_utf8_lossy(&stderr_buf),
Err(e) => {
tracing::warn!("failed seek+read: {e}");
"<failed to read stderr>".into()
}
};
(&*r).to_owned()
}

impl ExitStatusExt for std::process::ExitStatus {
fn check_status(&mut self, stderr: std::fs::File) -> Result<()> {
let stderr_buf = last_utf8_content_from_file(stderr);
if self.success() {
return Ok(());
}
anyhow::bail!(format!("Subprocess failed: {self:?}\n{stderr_buf}"))
}
}

impl CommandRunExt for Command {
/// Synchronously execute the child, and return an error if the child exited unsuccessfully.
fn run(&mut self) -> Result<()> {
let st = self.status()?;
if !st.success() {
// Note that we intentionally *don't* include the command string
// in the output; we leave it to the caller to add that if they want,
// as it may be verbose.
anyhow::bail!(format!("Subprocess failed: {st:?}"))
}
Ok(())
let stderr = tempfile::tempfile()?;
self.stderr(stderr.try_clone()?);
self.status()?.check_status(stderr)
}
}

/// Helpers intended for [`tokio::process::Command`].
#[allow(dead_code)]
pub(crate) trait AsyncCommandRunExt {
async fn run(&mut self) -> Result<()>;
}

impl AsyncCommandRunExt for tokio::process::Command {
/// Asynchronously execute the child, and return an error if the child exited unsuccessfully.
///
async fn run(&mut self) -> Result<()> {
let stderr = tempfile::tempfile()?;
self.stderr(stderr.try_clone()?);
self.status().await?.check_status(stderr)
}
}

Expand Down Expand Up @@ -132,14 +194,14 @@ pub(crate) fn medium_visibility_warning(s: &str) {
/// 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: &'static str, f: F) -> T
pub(crate) async fn async_task_with_spinner<F, T>(msg: &str, f: F) -> T
where
F: Future<Output = T>,
{
let pb = indicatif::ProgressBar::new_spinner();
let style = indicatif::ProgressStyle::default_bar();
pb.set_style(style.template("{spinner} {msg}").unwrap());
pb.set_message(msg);
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.
Expand Down Expand Up @@ -212,6 +274,43 @@ fn test_sigpolicy_from_opts() {

#[test]
fn command_run_ext() {
// The basics
Command::new("true").run().unwrap();
assert!(Command::new("false").run().is_err());

// Verify we capture stderr
let e = Command::new("/bin/sh")
.args(["-c", "echo expected-this-oops-message 1>&2; exit 1"])
.run()
.err()
.unwrap();
similar_asserts::assert_eq!(
e.to_string(),
"Subprocess failed: ExitStatus(unix_wait_status(256))\nexpected-this-oops-message\n"
);

// Ignoring invalid UTF-8
let e = Command::new("/bin/sh")
.args([
"-c",
r"echo -e 'expected\xf5\x80\x80\x80\x80-foo\xc0bar\xc0\xc0' 1>&2; exit 1",
])
.run()
.err()
.unwrap();
similar_asserts::assert_eq!(
e.to_string(),
"Subprocess failed: ExitStatus(unix_wait_status(256))\nexpected�����-foo�bar��\n"
);
}

#[tokio::test]
async fn async_command_run_ext() {
use tokio::process::Command as AsyncCommand;
let mut success = AsyncCommand::new("true");
let mut fail = AsyncCommand::new("false");
// Run these in parallel just because we can
let (success, fail) = tokio::join!(success.run(), fail.run(),);
success.unwrap();
assert!(fail.is_err());
}
11 changes: 11 additions & 0 deletions tests-integration/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::path::Path;
use std::{os::fd::AsRawFd, path::PathBuf};

use anyhow::Result;
use camino::Utf8Path;
use cap_std_ext::cap_std;
use cap_std_ext::cap_std::fs::Dir;
use fn_error_context::context;
Expand Down Expand Up @@ -53,6 +54,12 @@ fn find_deployment_root() -> Result<Dir> {
anyhow::bail!("Failed to find deployment root")
}

// Hook relatively cheap post-install tests here
fn generic_post_install_verification() -> Result<()> {
assert!(Utf8Path::new("/ostree/repo").try_exists()?);
Ok(())
}

#[context("Install tests")]
pub(crate) fn run_alongside(image: &str, mut testargs: libtest_mimic::Arguments) -> Result<()> {
// Force all of these tests to be serial because they mutate global state
Expand Down Expand Up @@ -88,6 +95,8 @@ pub(crate) fn run_alongside(image: &str, mut testargs: libtest_mimic::Arguments)
std::fs::write(&tmp_keys, b"ssh-ed25519 ABC0123 [email protected]")?;
cmd!(sh, "sudo {BASE_ARGS...} {target_args...} -v {tmp_keys}:/test_authorized_keys {image} bootc install to-filesystem {generic_inst_args...} --acknowledge-destructive --karg=foo=bar --replace=alongside --root-ssh-authorized-keys=/test_authorized_keys /target").run()?;

generic_post_install_verification()?;

// Test kargs injected via CLI
cmd!(
sh,
Expand Down Expand Up @@ -120,6 +129,7 @@ pub(crate) fn run_alongside(image: &str, mut testargs: libtest_mimic::Arguments)
let sh = &xshell::Shell::new()?;
reset_root(sh)?;
cmd!(sh, "sudo {BASE_ARGS...} {target_args...} {image} bootc install to-existing-root --acknowledge-destructive {generic_inst_args...}").run()?;
generic_post_install_verification()?;
let root = &Dir::open_ambient_dir("/ostree", cap_std::ambient_authority()).unwrap();
let mut path = PathBuf::from(".");
crate::selinux::verify_selinux_recurse(root, &mut path, false)?;
Expand All @@ -131,6 +141,7 @@ pub(crate) fn run_alongside(image: &str, mut testargs: libtest_mimic::Arguments)
let empty = sh.create_temp_dir()?;
let empty = empty.path().to_str().unwrap();
cmd!(sh, "sudo {BASE_ARGS...} {target_args...} -v {empty}:/usr/lib/bootc/install {image} bootc install to-existing-root {generic_inst_args...}").run()?;
generic_post_install_verification()?;
Ok(())
}),
];
Expand Down

0 comments on commit 7520fa4

Please sign in to comment.