#![forbid(unsafe_code)]
mod bulk;
mod cli;
mod clients;
mod commands;
mod debug;
mod env;
mod error;
mod files;
mod input;
mod json;
mod output;
mod pagination;
mod policy;
mod render;
mod signals;
mod store;
mod tenant;
mod views;
mod writes;
use clap::FromArgMatches;
use commands::{Invocation, Plan};
use error::{CliError, EXIT_OK, EXIT_OPERATIONAL, EXIT_USAGE};
use output::Output;
use std::ffi::OsString;
use std::io::{ErrorKind, Write};
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub fn run<I, T>(args: I) -> i32
where
I: IntoIterator<Item = T>,
T: Into<OsString>,
{
let args: Vec<OsString> = args.into_iter().map(Into::into).collect();
if version_requested(&args) {
return print_version();
}
let parsed = cli::command()
.try_get_matches_from(&args)
.and_then(|matches| cli::Cli::from_arg_matches(&matches));
let cli = match parsed {
Ok(cli) => cli,
Err(error) => {
let error = cli::redacted_parse_error(&args, &error).unwrap_or(error);
let _ = error.print();
return error.exit_code();
}
};
let mut output = Output::stdio(cli.global.quiet);
output.set_debug(cli.global.debug);
if commands::uses_format_alias(&cli) {
output.warning(commands::FORMAT_DEPRECATED);
}
if commands::uses_input_alias(&cli) {
output.warning(commands::INPUT_DEPRECATED);
}
let plan = match commands::plan(&cli) {
Ok(plan) => plan,
Err(error) => {
output.report(&error);
return error.exit_code(None);
}
};
if let Plan::GroupHelp(path) = plan {
output.write_stderr(&cli::group_help(path));
return EXIT_USAGE;
}
let mut invocation = Invocation::new(output, cli.global.tenant.clone());
if cli.global.debug
&& plan.debug_policy() == debug::DebugPolicy::Log
&& let Err(error) = invocation.open_debug_log()
{
invocation.output.report(&error);
return error.exit_code(None);
}
execute(plan, invocation)
}
pub fn version_requested(args: &[OsString]) -> bool {
args.iter()
.skip(1)
.take_while(|arg| *arg != "--")
.any(|arg| arg == "-V" || arg == "--version")
}
fn stdout_write_failed() -> CliError {
CliError::operational("AISEC_SDK_ERROR:Output could not be written to stdout")
}
fn print_version() -> i32 {
let mut stdout = std::io::stdout().lock();
match writeln!(stdout, "{VERSION}").and_then(|()| stdout.flush()) {
Err(error) if error.kind() != ErrorKind::BrokenPipe => {
let text = stdout_write_failed().render_colored(render::style::stderr_color());
let _ = std::io::stderr().write_all(text.as_bytes());
EXIT_OPERATIONAL
}
_ => EXIT_OK,
}
}
fn execute(plan: Plan, mut invocation: Invocation) -> i32 {
let runtime = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(runtime) => runtime,
Err(_) => {
let error = CliError::operational("AISEC_SDK_ERROR:The async runtime could not start");
invocation.output.report(&error);
return EXIT_OPERATIONAL;
}
};
let signals = signals::install(invocation.session.clone());
signals.set_quiet(invocation.output.quiet());
invocation.output.attach_signals(signals.clone());
let fallback = plan.interrupt_context();
let result = runtime.block_on(commands::dispatch(&mut invocation, plan));
let fallback = if invocation.committed {
error::InterruptContext::ChangeSaved
} else {
invocation.interrupt.take().unwrap_or(fallback)
};
runtime.shutdown_background();
let signal = signals.code();
let result = match (result, signal) {
(Err(CliError::Interrupted(context)), _) => Err(CliError::Interrupted(context)),
(_, Some(_)) => Err(CliError::Interrupted(fallback)),
(result, None) => result,
};
finish(&mut invocation.output, result, signal)
}
fn finish(output: &mut Output, result: Result<(), CliError>, signal: Option<i32>) -> i32 {
match result {
Ok(()) => match output.commit() {
Ok(()) => EXIT_OK,
Err(error) if error.kind() == ErrorKind::BrokenPipe => EXIT_OK,
Err(_) => {
let error = match output.pending_signal() {
Some(_) => CliError::Interrupted(error::InterruptContext::OutputIncomplete),
None => stdout_write_failed(),
};
output.report(&error);
error.exit_code(output.pending_signal())
}
},
Err(error) => {
output.discard();
output.report(&error);
error.exit_code(signal)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn args(list: &[&str]) -> Vec<OsString> {
list.iter().map(OsString::from).collect()
}
#[test]
fn version_pre_scan() {
assert!(version_requested(&args(&["airs-cli-rs", "-V"])));
assert!(version_requested(&args(&[
"airs-cli-rs",
"runtime",
"scan",
"--profile",
"-V",
"hi"
])));
assert!(version_requested(&args(&[
"airs-cli-rs",
"doctor",
"--version"
])));
assert!(!version_requested(&args(&[
"airs-cli-rs",
"runtime",
"scan",
"--profile",
"p",
"--",
"-V"
])));
assert!(
!version_requested(&args(&["-V"])),
"the program name is skipped"
);
assert!(!version_requested(&args(&["airs-cli-rs", "-v"])));
}
#[test]
fn finish_maps_results_to_exit_codes() {
let (mut out, stdout, stderr) = output::testing::capture(false);
out.data_line("data");
assert_eq!(finish(&mut out, Ok(()), None), 0);
assert_eq!(stdout.text(), "data\n");
let (mut out, stdout, stderr2) = output::testing::capture(false);
out.data_line("partial");
assert_eq!(finish(&mut out, Err(CliError::usage("bad")), None), 2);
assert_eq!(stdout.text(), "");
assert_eq!(stderr2.text(), "\n ✗ bad\n\n");
let (mut out, _, stderr3) = output::testing::capture(false);
let code = finish(
&mut out,
Err(CliError::Interrupted(
error::InterruptContext::NoDataWritten,
)),
Some(143),
);
assert_eq!(code, 143);
assert!(stderr3.text().contains("Interrupted; no data was written"));
assert_eq!(stderr.text(), "");
}
#[test]
fn broken_pipe_is_success() {
struct Closed;
impl Write for Closed {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::from(ErrorKind::BrokenPipe))
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut out = Output::with_sinks(false, Box::new(Closed), Box::new(std::io::sink()));
out.data_line("data");
assert_eq!(finish(&mut out, Ok(()), None), 0);
}
#[test]
fn other_stdout_write_failures_are_reported() {
struct Full;
impl Write for Full {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::from(ErrorKind::StorageFull))
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let stderr = output::testing::Capture::default();
let mut out = Output::with_sinks(false, Box::new(Full), Box::new(stderr.clone()));
out.data_line("data");
assert_eq!(finish(&mut out, Ok(()), None), 1);
assert_eq!(
stderr.text(),
"\n ✗ Error: AISEC_SDK_ERROR:Output could not be written to stdout\n\n"
);
}
#[test]
fn a_signal_before_the_final_write_reports_the_interruption() {
let (mut out, stdout, stderr) = output::testing::capture(false);
let signals = std::sync::Arc::new(signals::SignalState::default());
assert_eq!(signals.on_signal(143), signals::Action::Cancel);
out.attach_signals(signals);
out.data_line("data");
assert_eq!(finish(&mut out, Ok(()), None), 143);
assert_eq!(stdout.text(), "", "nothing is written after a signal");
assert_eq!(
stderr.text(),
"\n ✗ Error: Interrupted; the output was not completely written\n\n"
);
}
}