Organize the public CLI with clap
This commit is contained in:
parent
e7dd95aea6
commit
d08d931b5f
1 changed files with 445 additions and 255 deletions
|
|
@ -1,7 +1,5 @@
|
|||
use std::ffi::OsString;
|
||||
use std::io::{self, Write};
|
||||
use std::os::fd::AsFd;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use lazaret_core::records::Domain;
|
||||
use lazaret_host_coordinator::domain_store::{HostCoordinatorDomainStore, StateResetScopeV1};
|
||||
|
|
@ -19,19 +17,20 @@ use crate::coordinator_backend::{load_configuration, open_private_domain_lock};
|
|||
const OUTPUT_SCHEMA: &str = "lazaret-state-output-v1";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum Operation {
|
||||
pub enum StateOperation {
|
||||
List,
|
||||
Inspect,
|
||||
Verify,
|
||||
Reset,
|
||||
}
|
||||
|
||||
struct StateCommand {
|
||||
operation: Operation,
|
||||
context: ClientCommand,
|
||||
machine: bool,
|
||||
yes: bool,
|
||||
scope: Option<StateResetScopeV1>,
|
||||
#[derive(Debug)]
|
||||
pub struct StateCommand {
|
||||
pub(crate) operation: StateOperation,
|
||||
pub(crate) context: ClientCommand,
|
||||
pub(crate) machine: bool,
|
||||
pub(crate) yes: bool,
|
||||
pub(crate) scope: Option<StateResetScopeV1>,
|
||||
}
|
||||
|
||||
/// Runs one state-maintenance command using resolved configuration and exclusive Domain authority.
|
||||
|
|
@ -39,8 +38,7 @@ struct StateCommand {
|
|||
/// # Errors
|
||||
/// Refuses invalid arguments, active Domains, arbitrary targets, authority changes, malformed
|
||||
/// durable state, structural failures, and reset without confirmation.
|
||||
pub fn run_command(arguments: &[OsString]) -> Result<i32, Box<dyn std::error::Error>> {
|
||||
let command = parse(arguments)?;
|
||||
pub fn run_command(command: StateCommand) -> Result<i32, Box<dyn std::error::Error>> {
|
||||
let configuration = load_configuration(&command.context)?;
|
||||
if let Some(StateResetScopeV1::Selected(path)) = &command.scope
|
||||
&& !configuration
|
||||
|
|
@ -78,10 +76,10 @@ pub fn run_command(arguments: &[OsString]) -> Result<i32, Box<dyn std::error::Er
|
|||
};
|
||||
let store = HostCoordinatorDomainStore::open_file(lock, database.into(), &expected)?;
|
||||
let report = match command.operation {
|
||||
Operation::List | Operation::Inspect | Operation::Verify => {
|
||||
StateOperation::List | StateOperation::Inspect | StateOperation::Verify => {
|
||||
inspect_domain_state(&configuration, &store)?
|
||||
}
|
||||
Operation::Reset => {
|
||||
StateOperation::Reset => {
|
||||
let scope = command.scope.ok_or("reset scope is missing")?;
|
||||
if !command.yes {
|
||||
confirm(&configuration, &scope)?;
|
||||
|
|
@ -93,97 +91,6 @@ pub fn run_command(arguments: &[OsString]) -> Result<i32, Box<dyn std::error::Er
|
|||
Ok(0)
|
||||
}
|
||||
|
||||
fn parse(arguments: &[OsString]) -> Result<StateCommand, &'static str> {
|
||||
let operation = match arguments.first().and_then(|value| value.to_str()) {
|
||||
Some("list") => Operation::List,
|
||||
Some("inspect") => Operation::Inspect,
|
||||
Some("verify") => Operation::Verify,
|
||||
Some("reset") => Operation::Reset,
|
||||
_ => return Err("expected `state list|inspect|verify|reset`"),
|
||||
};
|
||||
let mut profile = None;
|
||||
let mut workspace = None;
|
||||
let mut configuration = None;
|
||||
let mut machine = false;
|
||||
let mut yes = false;
|
||||
let mut scope = None;
|
||||
let mut index = 1;
|
||||
while index < arguments.len() {
|
||||
let option = arguments[index].to_str().ok_or("options must be UTF-8")?;
|
||||
index += 1;
|
||||
match option {
|
||||
"--profile" => profile = Some(value(arguments, &mut index, "--profile")?),
|
||||
"--workspace" => workspace = Some(PathBuf::from(value_os(arguments, &mut index)?)),
|
||||
"--config" => configuration = Some(PathBuf::from(value_os(arguments, &mut index)?)),
|
||||
"--json" => machine = true,
|
||||
"--yes" => yes = true,
|
||||
"--all" => set_scope(&mut scope, StateResetScopeV1::All)?,
|
||||
"--path" => {
|
||||
let path = value(arguments, &mut index, "--path")?;
|
||||
set_scope(&mut scope, StateResetScopeV1::Selected(path.into_bytes()))?;
|
||||
}
|
||||
_ => return Err("unknown state-maintenance option"),
|
||||
}
|
||||
}
|
||||
let profile = profile.ok_or("`--profile <name>` is required")?;
|
||||
let workspace = match workspace {
|
||||
Some(workspace) => workspace,
|
||||
None => std::env::current_dir().map_err(|_| "cannot determine current workspace")?,
|
||||
};
|
||||
if operation == Operation::Reset && scope.is_none() {
|
||||
return Err("reset requires exactly one of `--all` or `--path <container-path>`");
|
||||
}
|
||||
if operation != Operation::Reset && (scope.is_some() || yes) {
|
||||
return Err("reset scope and confirmation options require `state reset`");
|
||||
}
|
||||
Ok(StateCommand {
|
||||
operation,
|
||||
context: ClientCommand {
|
||||
profile,
|
||||
workspace,
|
||||
configuration,
|
||||
argv: Vec::new(),
|
||||
},
|
||||
machine,
|
||||
yes,
|
||||
scope,
|
||||
})
|
||||
}
|
||||
|
||||
fn value(
|
||||
arguments: &[OsString],
|
||||
index: &mut usize,
|
||||
option: &'static str,
|
||||
) -> Result<String, &'static str> {
|
||||
value_os(arguments, index)?
|
||||
.into_string()
|
||||
.map_err(|_| match option {
|
||||
"--profile" => "profile name must be UTF-8",
|
||||
"--path" => "container path must be UTF-8",
|
||||
_ => "option value must be UTF-8",
|
||||
})
|
||||
}
|
||||
|
||||
fn value_os(arguments: &[OsString], index: &mut usize) -> Result<OsString, &'static str> {
|
||||
let value = arguments
|
||||
.get(*index)
|
||||
.ok_or("option requires a value")?
|
||||
.clone();
|
||||
*index += 1;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn set_scope(
|
||||
current: &mut Option<StateResetScopeV1>,
|
||||
scope: StateResetScopeV1,
|
||||
) -> Result<(), &'static str> {
|
||||
if current.replace(scope).is_some() {
|
||||
Err("reset accepts exactly one scope")
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(
|
||||
configuration: &lazaret_core::configuration::ResolvedDomainConfiguration,
|
||||
scope: &StateResetScopeV1,
|
||||
|
|
@ -211,7 +118,7 @@ fn confirm(
|
|||
}
|
||||
}
|
||||
|
||||
fn write_report(report: &DomainStateReport, operation: Operation, machine: bool) {
|
||||
fn write_report(report: &DomainStateReport, operation: StateOperation, machine: bool) {
|
||||
if machine {
|
||||
let anchors = report
|
||||
.anchors
|
||||
|
|
@ -252,7 +159,7 @@ fn write_report(report: &DomainStateReport, operation: Operation, machine: bool)
|
|||
anchor.container_path,
|
||||
anchor.changes.len()
|
||||
);
|
||||
if matches!(operation, Operation::Inspect | Operation::Verify) {
|
||||
if matches!(operation, StateOperation::Inspect | StateOperation::Verify) {
|
||||
for entry in &anchor.changes {
|
||||
println!(
|
||||
" {} {} mode {:o} uid {} gid {} size {}",
|
||||
|
|
@ -270,12 +177,12 @@ fn write_report(report: &DomainStateReport, operation: Operation, machine: bool)
|
|||
}
|
||||
}
|
||||
|
||||
const fn operation_name(operation: Operation) -> &'static str {
|
||||
const fn operation_name(operation: StateOperation) -> &'static str {
|
||||
match operation {
|
||||
Operation::List => "list",
|
||||
Operation::Inspect => "inspect",
|
||||
Operation::Verify => "verify",
|
||||
Operation::Reset => "reset",
|
||||
StateOperation::List => "list",
|
||||
StateOperation::Inspect => "inspect",
|
||||
StateOperation::Verify => "verify",
|
||||
StateOperation::Reset => "reset",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -295,43 +202,3 @@ fn hex(bytes: &[u8]) -> String {
|
|||
output
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parser_requires_explicit_reset_scope_and_rejects_arbitrary_options() {
|
||||
assert!(parse(&["reset", "--profile", "default"].map(OsString::from)).is_err());
|
||||
assert!(
|
||||
parse(
|
||||
&["reset", "--profile", "default", "--all", "--path", "/usr",].map(OsString::from)
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(parse(&["inspect", "--profile", "default", "--yes"].map(OsString::from)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_preserves_only_container_scope_not_a_host_upper_path() {
|
||||
let command = parse(
|
||||
&[
|
||||
"reset",
|
||||
"--profile",
|
||||
"default",
|
||||
"--path",
|
||||
"/usr",
|
||||
"--yes",
|
||||
"--json",
|
||||
]
|
||||
.map(OsString::from),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
command.scope,
|
||||
Some(StateResetScopeV1::Selected(b"/usr".to_vec()))
|
||||
);
|
||||
assert!(command.yes);
|
||||
assert!(command.machine);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue