mirror of
https://github.com/containers/bootc.git
synced 2026-02-05 06:45:13 +01:00
cli: add system-reinstall-bootc binary
# Background The current usage instructions for bootc involve a long podman invocation. # Issue It's hard to remember and type the long podman invocation, making the usage of bootc difficult for users. See https://issues.redhat.com/browse/BIFROST-610 and https://issues.redhat.com/browse/BIFROST-611 (Epic https://issues.redhat.com/browse/BIFROST-594) # Solution We want to make the usage of bootc easier by providing a new Fedora/RHEL subpackage that includes a new binary `system-reinstall-bootc`. This binary will simplify the usage of bootc by providing a simple command line interface (configured either through CLI flags or a configuration file) with an interactive prompt that allows users to reinstall the current system using bootc. The commandline will handle helping the user choose SSH keys / users, warn the user about the destructive nature of the operation, and eventually report issues they might run into in the various clouds (e.g. missing cloud agent on the target image) # Implementation Added new system-reinstall-bootc crate that outputs the new system-reinstall-bootc binary. This new crate depends on the existing utils crate. Refactored the tracing initialization from the bootc binary into the utils crate so that it can be reused by the new crate. The new CLI can either be configured through commandline flags or through a configuration file in a path set by the environment variable `BOOTC_REINSTALL_CONFIG`. The configuration file is a YAML file. # Limitations Only root SSH keys are supported. The multi user selection TUI is implemented, but if you choose anything other than root you will get an error. # TODO Missing docs, missing functionality. Everything is in alpha stage. User choice / SSH keys / prompt disabling should also eventually be supported to be configured through commandline arguments or the configuration file. Signed-off-by: Omer Tuchfeld <omer@tuchfeld.dev>
This commit is contained in:
52
Cargo.lock
generated
52
Cargo.lock
generated
@@ -637,6 +637,19 @@ version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00"
|
||||
|
||||
[[package]]
|
||||
name = "dialoguer"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de"
|
||||
dependencies = [
|
||||
"console",
|
||||
"shell-words",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -1951,6 +1964,12 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -2083,6 +2102,23 @@ dependencies = [
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-reinstall-bootc"
|
||||
version = "0.1.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bootc-utils",
|
||||
"clap",
|
||||
"dialoguer",
|
||||
"log",
|
||||
"rustix",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"tracing",
|
||||
"uzers",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.43"
|
||||
@@ -2418,6 +2454,16 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uzers"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4df81ff504e7d82ad53e95ed1ad5b72103c11253f39238bcc0235b90768a97dd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
@@ -2727,6 +2773,12 @@ dependencies = [
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.2"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"cli",
|
||||
"system-reinstall-bootc",
|
||||
"lib",
|
||||
"ostree-ext",
|
||||
"utils",
|
||||
|
||||
1
Makefile
1
Makefile
@@ -9,6 +9,7 @@ all:
|
||||
|
||||
install:
|
||||
install -D -m 0755 -t $(DESTDIR)$(prefix)/bin target/release/bootc
|
||||
install -D -m 0755 -t $(DESTDIR)$(prefix)/bin target/release/system-reinstall-bootc
|
||||
install -d -m 0755 $(DESTDIR)$(prefix)/lib/bootc/bound-images.d
|
||||
install -d -m 0755 $(DESTDIR)$(prefix)/lib/bootc/kargs.d
|
||||
ln -s /sysroot/ostree/bootc/storage $(DESTDIR)$(prefix)/lib/bootc/storage
|
||||
|
||||
31
system-reinstall-bootc/Cargo.toml
Normal file
31
system-reinstall-bootc/Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "system-reinstall-bootc"
|
||||
version = "0.1.9"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/containers/bootc"
|
||||
readme = "README.md"
|
||||
publish = false
|
||||
# For now don't bump this above what is currently shipped in RHEL9.
|
||||
rust-version = "1.75.0"
|
||||
|
||||
# See https://github.com/coreos/cargo-vendor-filterer
|
||||
[package.metadata.vendor-filter]
|
||||
# For now we only care about tier 1+2 Linux. (In practice, it's unlikely there is a tier3-only Linux dependency)
|
||||
platforms = ["*-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
bootc-utils = { path = "../utils" }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
dialoguer = "0.11.0"
|
||||
log = "0.4.21"
|
||||
rustix = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = "0.9.22"
|
||||
tracing = { workspace = true }
|
||||
uzers = "0.12.1"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
2
system-reinstall-bootc/sample_config.yaml
Normal file
2
system-reinstall-bootc/sample_config.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
# The bootc container image to install
|
||||
bootc_image: quay.io/fedora/fedora-bootc:41
|
||||
7
system-reinstall-bootc/src/config/cli.rs
Normal file
7
system-reinstall-bootc/src/config/cli.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct Cli {
|
||||
/// The bootc container image to install, e.g. quay.io/fedora/fedora-bootc:41
|
||||
pub(crate) bootc_image: String,
|
||||
}
|
||||
52
system-reinstall-bootc/src/config/mod.rs
Normal file
52
system-reinstall-bootc/src/config/mod.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use anyhow::{ensure, Context, Result};
|
||||
use clap::Parser;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod cli;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct ReinstallConfig {
|
||||
/// The bootc image to install on the system.
|
||||
pub(crate) bootc_image: String,
|
||||
|
||||
/// The raw CLI arguments that were used to invoke the program. None if the config was loaded
|
||||
/// from a file.
|
||||
#[serde(skip_deserializing)]
|
||||
cli_flags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl ReinstallConfig {
|
||||
pub fn parse_from_cli(cli: cli::Cli) -> Self {
|
||||
Self {
|
||||
bootc_image: cli.bootc_image,
|
||||
cli_flags: Some(std::env::args().collect::<Vec<String>>()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load() -> Result<Self> {
|
||||
Ok(match std::env::var("BOOTC_REINSTALL_CONFIG") {
|
||||
Ok(config_path) => {
|
||||
ensure_no_cli_args()?;
|
||||
|
||||
serde_yaml::from_slice(
|
||||
&std::fs::read(&config_path)
|
||||
.context("reading BOOTC_REINSTALL_CONFIG file {config_path}")?,
|
||||
)
|
||||
.context("parsing BOOTC_REINSTALL_CONFIG file {config_path}")?
|
||||
}
|
||||
Err(_) => ReinstallConfig::parse_from_cli(cli::Cli::parse()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_no_cli_args() -> Result<()> {
|
||||
let num_args = std::env::args().len();
|
||||
|
||||
ensure!(
|
||||
num_args == 1,
|
||||
"BOOTC_REINSTALL_CONFIG is set, but there are {num_args} CLI arguments. BOOTC_REINSTALL_CONFIG is meant to be used with no arguments."
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
47
system-reinstall-bootc/src/main.rs
Normal file
47
system-reinstall-bootc/src/main.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
//! The main entrypoint for the bootc system reinstallation CLI
|
||||
|
||||
use anyhow::{ensure, Context, Result};
|
||||
use bootc_utils::CommandRunExt;
|
||||
use rustix::process::getuid;
|
||||
|
||||
mod config;
|
||||
mod podman;
|
||||
mod prompt;
|
||||
pub(crate) mod users;
|
||||
|
||||
const ROOT_KEY_MOUNT_POINT: &str = "/bootc_authorized_ssh_keys/root";
|
||||
|
||||
fn run() -> Result<()> {
|
||||
bootc_utils::initialize_tracing();
|
||||
tracing::trace!("starting {}", env!("CARGO_PKG_NAME"));
|
||||
|
||||
// Rootless podman is not supported by bootc
|
||||
ensure!(getuid().is_root(), "Must run as the root user");
|
||||
|
||||
let config = config::ReinstallConfig::load().context("loading config")?;
|
||||
|
||||
let mut reinstall_podman_command =
|
||||
podman::command(&config.bootc_image, &prompt::get_root_key()?);
|
||||
|
||||
println!();
|
||||
|
||||
println!("Going to run command {:?}", reinstall_podman_command);
|
||||
|
||||
prompt::temporary_developer_protection_prompt()?;
|
||||
|
||||
reinstall_podman_command
|
||||
.run_with_cmd_context()
|
||||
.context("running reinstall command")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// In order to print the error in a custom format (with :#) our
|
||||
// main simply invokes a run() where all the work is done.
|
||||
// This code just captures any errors.
|
||||
if let Err(e) = run() {
|
||||
tracing::error!("{:#}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
60
system-reinstall-bootc/src/podman.rs
Normal file
60
system-reinstall-bootc/src/podman.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use std::process::Command;
|
||||
|
||||
use super::ROOT_KEY_MOUNT_POINT;
|
||||
use crate::users::UserKeys;
|
||||
|
||||
pub(crate) fn command(image: &str, root_key: &Option<UserKeys>) -> Command {
|
||||
let mut podman_command_and_args = [
|
||||
// We use podman to run the bootc container. This might change in the future to remove the
|
||||
// podman dependency.
|
||||
"podman",
|
||||
"run",
|
||||
// The container needs to be privileged, as it heavily modifies the host
|
||||
"--privileged",
|
||||
// The container needs to access the host's PID namespace to mount host directories
|
||||
"--pid=host",
|
||||
// Since https://github.com/containers/bootc/pull/919 this mount should not be needed, but
|
||||
// some reason with e.g. quay.io/fedora/fedora-bootc:41 it is still needed.
|
||||
"-v",
|
||||
"/var/lib/containers:/var/lib/containers",
|
||||
]
|
||||
.map(String::from)
|
||||
.to_vec();
|
||||
|
||||
let mut bootc_command_and_args = [
|
||||
"bootc",
|
||||
"install",
|
||||
// We're replacing the current root
|
||||
"to-existing-root",
|
||||
// The user already knows they're reinstalling their machine, that's the entire purpose of
|
||||
// this binary. Since this is no longer an "arcane" bootc command, we can safely avoid this
|
||||
// timed warning prompt. TODO: Discuss in https://github.com/containers/bootc/discussions/1060
|
||||
"--acknowledge-destructive",
|
||||
]
|
||||
.map(String::from)
|
||||
.to_vec();
|
||||
|
||||
if let Some(root_key) = root_key.as_ref() {
|
||||
let root_authorized_keys_path = root_key.authorized_keys_path.clone();
|
||||
|
||||
podman_command_and_args.push("-v".to_string());
|
||||
podman_command_and_args.push(format!(
|
||||
"{root_authorized_keys_path}:{ROOT_KEY_MOUNT_POINT}"
|
||||
));
|
||||
|
||||
bootc_command_and_args.push("--root-ssh-authorized-keys".to_string());
|
||||
bootc_command_and_args.push(ROOT_KEY_MOUNT_POINT.to_string());
|
||||
}
|
||||
|
||||
let all_args = [
|
||||
podman_command_and_args,
|
||||
vec![image.to_string()],
|
||||
bootc_command_and_args,
|
||||
]
|
||||
.concat();
|
||||
|
||||
let mut command = Command::new(&all_args[0]);
|
||||
command.args(&all_args[1..]);
|
||||
|
||||
command
|
||||
}
|
||||
81
system-reinstall-bootc/src/prompt.rs
Normal file
81
system-reinstall-bootc/src/prompt.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use crate::users::{get_all_users_keys, UserKeys};
|
||||
use anyhow::{ensure, Context, Result};
|
||||
|
||||
fn prompt_single_user(user: &crate::users::UserKeys) -> Result<Vec<&crate::users::UserKeys>> {
|
||||
let prompt = format!(
|
||||
"Found only one user ({}) with {} SSH authorized keys. Would you like to install this user in the system?",
|
||||
user.user,
|
||||
user.num_keys(),
|
||||
);
|
||||
let answer = ask_yes_no(&prompt, true)?;
|
||||
Ok(if answer { vec![&user] } else { vec![] })
|
||||
}
|
||||
|
||||
fn prompt_user_selection(
|
||||
all_users: &[crate::users::UserKeys],
|
||||
) -> Result<Vec<&crate::users::UserKeys>> {
|
||||
let keys: Vec<String> = all_users.iter().map(|x| x.user.clone()).collect();
|
||||
|
||||
// TODO: Handle https://github.com/console-rs/dialoguer/issues/77
|
||||
let selected_user_indices: Vec<usize> = dialoguer::MultiSelect::new()
|
||||
.with_prompt("Select the users you want to install in the system (along with their authorized SSH keys)")
|
||||
.items(&keys)
|
||||
.interact()?;
|
||||
|
||||
Ok(selected_user_indices
|
||||
.iter()
|
||||
// Safe unwrap because we know the index is valid
|
||||
.map(|x| all_users.get(*x).unwrap())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Temporary safety mechanism to stop devs from running it on their dev machine. TODO: Discuss
|
||||
/// final prompting UX in https://github.com/containers/bootc/discussions/1060
|
||||
pub(crate) fn temporary_developer_protection_prompt() -> Result<()> {
|
||||
// Print an empty line so that the warning stands out from the rest of the output
|
||||
println!();
|
||||
|
||||
let prompt = "THIS WILL REINSTALL YOUR SYSTEM! Are you sure you want to continue?";
|
||||
let answer = ask_yes_no(prompt, false)?;
|
||||
|
||||
if !answer {
|
||||
println!("Exiting without reinstalling the system.");
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn ask_yes_no(prompt: &str, default: bool) -> Result<bool> {
|
||||
dialoguer::Confirm::new()
|
||||
.with_prompt(prompt)
|
||||
.default(default)
|
||||
.wait_for_newline(true)
|
||||
.interact()
|
||||
.context("prompting")
|
||||
}
|
||||
|
||||
/// For now we only support the root user. This function returns the root user's SSH
|
||||
/// authorized_keys. In the future, when bootc supports multiple users, this function will need to
|
||||
/// be updated to return the SSH authorized_keys for all the users selected by the user.
|
||||
pub(crate) fn get_root_key() -> Result<Option<UserKeys>> {
|
||||
let users = get_all_users_keys()?;
|
||||
if users.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let selected_users = if users.len() == 1 {
|
||||
prompt_single_user(&users[0])?
|
||||
} else {
|
||||
prompt_user_selection(&users)?
|
||||
};
|
||||
|
||||
ensure!(
|
||||
selected_users.iter().all(|x| x.user == "root"),
|
||||
"Only importing the root user keys is supported for now"
|
||||
);
|
||||
|
||||
let root_key = selected_users.into_iter().find(|x| x.user == "root");
|
||||
|
||||
Ok(root_key.cloned())
|
||||
}
|
||||
150
system-reinstall-bootc/src/users.rs
Normal file
150
system-reinstall-bootc/src/users.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
use anyhow::{Context, Result};
|
||||
use bootc_utils::CommandRunExt;
|
||||
use rustix::fs::Uid;
|
||||
use rustix::process::geteuid;
|
||||
use rustix::process::getuid;
|
||||
use rustix::thread::set_thread_res_uid;
|
||||
use serde_json::Value;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
use std::process::Command;
|
||||
use uzers::os::unix::UserExt;
|
||||
|
||||
fn loginctl_users() -> Result<Vec<String>> {
|
||||
let users: Value = Command::new("loginctl")
|
||||
.arg("list-sessions")
|
||||
.arg("--output")
|
||||
.arg("json")
|
||||
.run_and_parse_json()
|
||||
.context("loginctl failed")?;
|
||||
|
||||
users
|
||||
.as_array()
|
||||
.context("loginctl output is not an array")?
|
||||
.iter()
|
||||
.map(|user_value| {
|
||||
user_value
|
||||
.as_object()
|
||||
.context("user entry is not an object")?
|
||||
.get("user")
|
||||
.context("user object doesn't have a user field")?
|
||||
.as_str()
|
||||
.context("user name field is not a string")
|
||||
.map(String::from)
|
||||
})
|
||||
// Artificially add the root user to the list of users as it doesn't appear in loginctl
|
||||
// list-sessions
|
||||
.chain(std::iter::once(Ok("root".to_string())))
|
||||
.collect::<Result<Vec<String>>>()
|
||||
.context("error parsing users")
|
||||
}
|
||||
|
||||
struct UidChange {
|
||||
uid: Uid,
|
||||
euid: Uid,
|
||||
}
|
||||
|
||||
impl UidChange {
|
||||
fn new(change_to_uid: Uid) -> Result<Self> {
|
||||
let (uid, euid) = (getuid(), geteuid());
|
||||
set_thread_res_uid(uid, change_to_uid, euid).context("setting effective uid failed")?;
|
||||
Ok(Self { uid, euid })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for UidChange {
|
||||
fn drop(&mut self) {
|
||||
set_thread_res_uid(self.uid, self.euid, self.euid).expect("setting effective uid failed");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct UserKeys {
|
||||
pub(crate) user: String,
|
||||
pub(crate) authorized_keys: String,
|
||||
pub(crate) authorized_keys_path: String,
|
||||
}
|
||||
|
||||
impl UserKeys {
|
||||
pub(crate) fn num_keys(&self) -> usize {
|
||||
self.authorized_keys.lines().count()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for UserKeys {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"User {} ({} authorized keys)",
|
||||
self.user,
|
||||
self.num_keys()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_all_users_keys() -> Result<Vec<UserKeys>> {
|
||||
let loginctl_user_names = loginctl_users().context("enumerate users")?;
|
||||
|
||||
let mut all_users_authorized_keys = Vec::new();
|
||||
|
||||
for user_name in loginctl_user_names {
|
||||
let user_info = uzers::get_user_by_name(user_name.as_str())
|
||||
.context(format!("user {} not found", user_name))?;
|
||||
|
||||
let home_dir = user_info.home_dir();
|
||||
let user_authorized_keys_path = home_dir.join(".ssh/authorized_keys");
|
||||
|
||||
if !user_authorized_keys_path.exists() {
|
||||
tracing::debug!(
|
||||
"Skipping user {} because it doesn't have an SSH authorized_keys file",
|
||||
user_info.name().to_string_lossy()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let user_name = user_info
|
||||
.name()
|
||||
.to_str()
|
||||
.context("user name is not valid utf-8")?;
|
||||
|
||||
let user_authorized_keys = {
|
||||
// Safety: The UID should be valid because we got it from uzers
|
||||
#[allow(unsafe_code)]
|
||||
let user_uid = unsafe { Uid::from_raw(user_info.uid()) };
|
||||
|
||||
// Change the effective uid for this scope, to avoid accidentally reading files we
|
||||
// shouldn't through symlinks
|
||||
let _uid_change = UidChange::new(user_uid)?;
|
||||
|
||||
std::fs::read_to_string(&user_authorized_keys_path)
|
||||
.context("Failed to read user's authorized keys")?
|
||||
};
|
||||
|
||||
if user_authorized_keys.trim().is_empty() {
|
||||
tracing::debug!(
|
||||
"Skipping user {} because it has an empty SSH authorized_keys file",
|
||||
user_info.name().to_string_lossy()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let user_keys = UserKeys {
|
||||
user: user_name.to_string(),
|
||||
authorized_keys: user_authorized_keys,
|
||||
authorized_keys_path: user_authorized_keys_path
|
||||
.to_str()
|
||||
.context("user's authorized_keys path is not valid utf-8")?
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
tracing::debug!(
|
||||
"Found user {} with {} SSH authorized_keys",
|
||||
user_keys.user,
|
||||
user_keys.num_keys()
|
||||
);
|
||||
|
||||
all_users_authorized_keys.push(user_keys);
|
||||
}
|
||||
|
||||
Ok(all_users_authorized_keys)
|
||||
}
|
||||
@@ -16,6 +16,10 @@ pub trait CommandRunExt {
|
||||
/// Execute the child process.
|
||||
fn run(&mut self) -> Result<()>;
|
||||
|
||||
/// Execute the child process. In case of failure, include the command and its arguments in the
|
||||
/// error context
|
||||
fn run_with_cmd_context(&mut self) -> Result<()>;
|
||||
|
||||
/// Ensure the child does not outlive the parent.
|
||||
fn lifecycle_bind(&mut self) -> &mut Self;
|
||||
|
||||
@@ -133,6 +137,13 @@ impl CommandRunExt for Command {
|
||||
let output = self.run_get_output()?;
|
||||
serde_json::from_reader(output).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn run_with_cmd_context(&mut self) -> Result<()> {
|
||||
self.run()
|
||||
// The [`Debug`] output of command contains a properly shell-escaped commandline
|
||||
// representation that the user can copy paste into their shell
|
||||
.context("Failed to run command: {self:#?}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Helpers intended for [`tokio::process::Command`].
|
||||
|
||||
Reference in New Issue
Block a user