diff --git a/Cargo.lock b/Cargo.lock index 319e0f0..24cc75d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,7 +63,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "awscredx" -version = "0.6.1" +version = "0.7.0" dependencies = [ "ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 6aefa8a..2d8762c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awscredx" -version = "0.6.1" +version = "0.7.0" authors = ["Alexei Samokvalov "] edition = "2018" diff --git a/src/credentials.rs b/src/credentials.rs index fba5539..689ed51 100644 --- a/src/credentials.rs +++ b/src/credentials.rs @@ -65,6 +65,11 @@ impl Display for CredentialsProfile { } } +pub struct CredentialsData<'a> { + pub profile_name: &'a str, + pub expires_at: &'a Option>, +} + impl CredentialsFile { pub fn read>(path: P, expirations_path: P) -> Result { let mut cf = Self { @@ -172,6 +177,14 @@ impl CredentialsFile { .find(|p| p.profile_name == *profile_name) .map(|p| &p.credentials) } + + pub fn get_current_credentials_data(&self) -> impl Iterator + '_ { + self.profiles.iter() + .map(|x| CredentialsData{ + profile_name: &x.profile_name.0, + expires_at: x.credentials.expires_at(), + }) + } } fn read_profile_name(line: &str) -> Option<&str> { diff --git a/src/main.rs b/src/main.rs index cd1f82f..8d52ca0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,23 @@ +extern crate ansi_term; +extern crate chrono; extern crate clap; -extern crate rusoto_core; -extern crate rusoto_credential; -extern crate rusoto_sts; -extern crate toml; -extern crate serde; extern crate custom_error; -extern crate chrono; -extern crate ansi_term; -extern crate linked_hash_map; -extern crate reqwest; extern crate hyper; extern crate hyper_proxy; extern crate hyper_tls; +extern crate linked_hash_map; +extern crate reqwest; +extern crate rusoto_core; +extern crate rusoto_credential; +extern crate rusoto_sts; +extern crate serde; +extern crate toml; -use crate::config::Config; use ansi_term::{Color, Style}; +use chrono::{DateTime, Duration, Local}; + +use crate::config::Config; +use crate::credentials::CredentialsFile; mod config; mod state; @@ -25,36 +28,42 @@ mod version; mod util; fn main() { + const COMMAND_INIT: &str = "init"; + const COMMAND_ASSUME: &str = "assume"; + const COMMAND_LIST_PROFILES: &str = "list-profiles"; + const COMMAND_LIST_CREDENTIALS: &str = "list-credentials"; + const COMMAND_VERSION: &str = "version"; + let matches = clap::App::new("awscredx") .version(version::VERSION) .about(format!(r#"AWS credentials management, a.k.a. role assumption made easy. Run '{}' to create the configuration file and set up shell scripts."#, Style::new().fg(Color::Yellow).paint("awscredx init")).as_str()) - .subcommand(clap::SubCommand::with_name("assume") + .subcommand(clap::SubCommand::with_name(COMMAND_ASSUME) .about("Prints shell commands to assume the role for a given profile") .arg(clap::Arg::with_name("profile-name") .required(true) .help("Profile name which role to assume"))) - .subcommand(clap::SubCommand::with_name("init") + .subcommand(clap::SubCommand::with_name(COMMAND_INIT) .about("Initializes local environment")) - .subcommand(clap::SubCommand::with_name("list-profiles") + .subcommand(clap::SubCommand::with_name(COMMAND_LIST_PROFILES) .about("Lists configured profiles with their role ARNs")) - .subcommand(clap::SubCommand::with_name("version") + .subcommand(clap::SubCommand::with_name(COMMAND_LIST_CREDENTIALS) + .about("Lists current credentials with their expiration times")) + .subcommand(clap::SubCommand::with_name(COMMAND_VERSION) .about("Shows current version and checks for newer version")) .setting(clap::AppSettings::SubcommandRequiredElseHelp) .get_matches(); match matches.subcommand() { - ("assume", Some(arg)) => { + (COMMAND_ASSUME, Some(arg)) => { let config = read_config(); assume::run(arg.value_of("profile-name").unwrap(), &config) } - ("init", _) => - init::run(), - ("list-profiles", _) => - print_profiles(), - ("version", _) => - version::print_version(), + (COMMAND_INIT, _) => init::run(), + (COMMAND_LIST_PROFILES, _) => print_profiles(), + (COMMAND_LIST_CREDENTIALS, _) => print_credentials(), + (COMMAND_VERSION, _) => version::print_version(), _ => unreachable!(), } } @@ -79,11 +88,49 @@ fn print_profiles() { let max_profile_name = c.profiles .keys() .map(|x| x.as_ref().len()) - .max().unwrap(); + .max() + .unwrap_or(0); let width = max_profile_name + 2; println!("{:width$}Main profile", &c.main_profile, width = width); println!("{:width$}Main profile MFA session", &c.mfa_profile, width = width); for (name, prof) in c.profiles.iter() { println!("{:width$}{}", name, &prof.role_arn, width = width); } +} + +fn print_credentials() { + match CredentialsFile::read_default() { + Ok(cred_file) => { + let max_profile_width = cred_file.get_current_credentials_data() + .map(|x| x.profile_name.len()) + .max() + .unwrap_or(0); + let width = max_profile_width + 2; + let prof_style = Style::new().fg(Color::White).bold(); + let time_style = Style::new().fg(Color::Yellow); + for cred in cred_file.get_current_credentials_data() { + print!("{} expires ", + prof_style.paint(format!("{:width$}", cred.profile_name, width = width)), + ); + match cred.expires_at { + Some(time) => { + let local_time: DateTime = (*time).into(); + println!("at {} in {}", + time_style.paint(local_time.format("%H:%M").to_string()), + time_style.paint(format_duration(local_time - Local::now())) + ); + } + None => println!("{}", time_style.paint("never")), + } + } + } + Err(e) => { + println!("Cannot read credentials file: {}", e); + ::std::process::exit(3); + } + } +} + +fn format_duration(d: Duration) -> String { + format!("{}:{}", d.num_hours(), d.num_minutes() % 60) } \ No newline at end of file