|
|
|
@ -1,4 +1,3 @@
|
|
|
|
|
use std::env;
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use std::process::Command as ChildCommand;
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
@ -8,13 +7,11 @@ use std::time::Duration;
|
|
|
|
|
|
|
|
|
|
use anyhow::{bail, Context};
|
|
|
|
|
use clap::{
|
|
|
|
|
Command,
|
|
|
|
|
Arg,
|
|
|
|
|
ArgMatches,
|
|
|
|
|
ArgAction,
|
|
|
|
|
builder::PossibleValuesParser,
|
|
|
|
|
value_parser,
|
|
|
|
|
Args,
|
|
|
|
|
Parser,
|
|
|
|
|
Subcommand
|
|
|
|
|
};
|
|
|
|
|
use clap::builder::styling::{Styles, AnsiColor};
|
|
|
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
|
|
|
|
|
|
|
|
use crate::proto::{
|
|
|
|
@ -26,80 +23,97 @@ use crate::proto::{
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn parser() -> Command<'static> {
|
|
|
|
|
Command::new("creddy")
|
|
|
|
|
.version(env!("CARGO_PKG_VERSION"))
|
|
|
|
|
.about("A friendly credential manager")
|
|
|
|
|
.arg(
|
|
|
|
|
Arg::new("server_addr")
|
|
|
|
|
.short('a')
|
|
|
|
|
.long("server-addr")
|
|
|
|
|
.takes_value(true)
|
|
|
|
|
.value_parser(value_parser!(PathBuf))
|
|
|
|
|
.help("Connect to the main Creddy process at this address")
|
|
|
|
|
)
|
|
|
|
|
.subcommand(
|
|
|
|
|
Command::new("run")
|
|
|
|
|
.about("Launch Creddy")
|
|
|
|
|
)
|
|
|
|
|
.subcommand(
|
|
|
|
|
Command::new("get")
|
|
|
|
|
.about("Request AWS credentials from Creddy and output to stdout")
|
|
|
|
|
.arg(
|
|
|
|
|
Arg::new("base")
|
|
|
|
|
.short('b')
|
|
|
|
|
.long("base")
|
|
|
|
|
.action(ArgAction::SetTrue)
|
|
|
|
|
.help("Use base credentials instead of session credentials")
|
|
|
|
|
)
|
|
|
|
|
.arg(
|
|
|
|
|
Arg::new("name")
|
|
|
|
|
.help("If unspecified, use default credentials")
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
.subcommand(
|
|
|
|
|
Command::new("exec")
|
|
|
|
|
.about("Inject AWS credentials into the environment of another command")
|
|
|
|
|
.trailing_var_arg(true)
|
|
|
|
|
.arg(
|
|
|
|
|
Arg::new("base")
|
|
|
|
|
.short('b')
|
|
|
|
|
.long("base")
|
|
|
|
|
.action(ArgAction::SetTrue)
|
|
|
|
|
.help("Use base credentials instead of session credentials")
|
|
|
|
|
)
|
|
|
|
|
.arg(
|
|
|
|
|
Arg::new("name")
|
|
|
|
|
.short('n')
|
|
|
|
|
.long("name")
|
|
|
|
|
.takes_value(true)
|
|
|
|
|
.help("If unspecified, use default credentials")
|
|
|
|
|
)
|
|
|
|
|
.arg(
|
|
|
|
|
Arg::new("command")
|
|
|
|
|
.multiple_values(true)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
.subcommand(
|
|
|
|
|
Command::new("shortcut")
|
|
|
|
|
.about("Invoke an action normally trigged by hotkey (e.g. launch terminal)")
|
|
|
|
|
.arg(
|
|
|
|
|
Arg::new("action")
|
|
|
|
|
.value_parser(
|
|
|
|
|
PossibleValuesParser::new(["show_window", "launch_terminal"])
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
#[derive(Debug, Parser)]
|
|
|
|
|
#[command(
|
|
|
|
|
about,
|
|
|
|
|
version,
|
|
|
|
|
name = "creddy",
|
|
|
|
|
bin_name = "creddy",
|
|
|
|
|
styles = Styles::styled()
|
|
|
|
|
.header(AnsiColor::Yellow.on_default())
|
|
|
|
|
.usage(AnsiColor::Yellow.on_default())
|
|
|
|
|
.literal(AnsiColor::Green.on_default())
|
|
|
|
|
.placeholder(AnsiColor::Green.on_default())
|
|
|
|
|
)]
|
|
|
|
|
/// A friendly credential manager
|
|
|
|
|
pub struct Cli {
|
|
|
|
|
#[command(flatten)]
|
|
|
|
|
pub global_args: GlobalArgs,
|
|
|
|
|
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
pub action: Option<Action>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Cli {
|
|
|
|
|
// proxy the Parser method so that main crate doesn't have to depend on Clap
|
|
|
|
|
pub fn parse() -> Self {
|
|
|
|
|
<Self as Parser>::parse()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn get(args: &ArgMatches, global_args: &ArgMatches) -> anyhow::Result<()> {
|
|
|
|
|
let name = args.get_one("name").cloned();
|
|
|
|
|
let base = *args.get_one("base").unwrap_or(&false);
|
|
|
|
|
let addr = global_args.get_one("server_addr").cloned();
|
|
|
|
|
#[derive(Debug, Clone, Args)]
|
|
|
|
|
pub struct GlobalArgs {
|
|
|
|
|
/// Connect to the main Creddy application at this path
|
|
|
|
|
#[arg(long, short = 'a')]
|
|
|
|
|
server_addr: Option<PathBuf>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let output = match make_request(addr, &CliRequest::GetCredential { name, base })?? {
|
|
|
|
|
CliResponse::Credential(c) => serde_json::to_string_pretty(&c).unwrap(),
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum Action {
|
|
|
|
|
/// Launch Creddy
|
|
|
|
|
Run,
|
|
|
|
|
/// Request credentials from Creddy and output to stdout
|
|
|
|
|
Get(GetArgs),
|
|
|
|
|
/// Inject credentials into the environment of another command
|
|
|
|
|
Exec(ExecArgs),
|
|
|
|
|
/// Invoke an action normally triggered by hotkey (e.g. launch terminal)
|
|
|
|
|
Shortcut(InvokeArgs),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Args)]
|
|
|
|
|
pub struct GetArgs {
|
|
|
|
|
/// If unspecified, use default credentials
|
|
|
|
|
#[arg(short, long)]
|
|
|
|
|
name: Option<String>,
|
|
|
|
|
/// Use base credentials instead of session credentials (only applicable to AWS)
|
|
|
|
|
#[arg(long, short, default_value_t = false)]
|
|
|
|
|
base: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Args)]
|
|
|
|
|
pub struct ExecArgs {
|
|
|
|
|
#[command(flatten)]
|
|
|
|
|
get_args: GetArgs,
|
|
|
|
|
#[arg(trailing_var_arg = true)]
|
|
|
|
|
/// Command to be wrapped
|
|
|
|
|
command: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Args)]
|
|
|
|
|
pub struct InvokeArgs {
|
|
|
|
|
#[arg(value_name = "ACTION", value_enum)]
|
|
|
|
|
shortcut_action: ShortcutAction,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn get(args: GetArgs, global: GlobalArgs) -> anyhow::Result<()> {
|
|
|
|
|
let req = CliRequest::GetCredential {
|
|
|
|
|
name: args.name,
|
|
|
|
|
base: args.base,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let output = match make_request(global.server_addr, &req)?? {
|
|
|
|
|
CliResponse::Credential(CliCredential::AwsBase(c)) => {
|
|
|
|
|
serde_json::to_string_pretty(&c).unwrap()
|
|
|
|
|
},
|
|
|
|
|
CliResponse::Credential(CliCredential::AwsSession(c)) => {
|
|
|
|
|
serde_json::to_string_pretty(&c).unwrap()
|
|
|
|
|
},
|
|
|
|
|
r => bail!("Unexpected response from server: {r}"),
|
|
|
|
|
};
|
|
|
|
|
println!("{output}");
|
|
|
|
@ -107,18 +121,20 @@ pub fn get(args: &ArgMatches, global_args: &ArgMatches) -> anyhow::Result<()> {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn exec(args: &ArgMatches, global_args: &ArgMatches) -> anyhow::Result<()> {
|
|
|
|
|
let name = args.get_one("name").cloned();
|
|
|
|
|
let base = *args.get_one("base").unwrap_or(&false);
|
|
|
|
|
let addr = global_args.get_one("server_addr").cloned();
|
|
|
|
|
pub fn exec(args: ExecArgs, global: GlobalArgs) -> anyhow::Result<()> {
|
|
|
|
|
// Clap guarantees that cmd_line will be a sequence of at least 1 item
|
|
|
|
|
// test this!
|
|
|
|
|
let mut cmd_line = args.get_many("command").unwrap();
|
|
|
|
|
let cmd_name: &String = cmd_line.next().unwrap();
|
|
|
|
|
let mut cmd_line = args.command.iter();
|
|
|
|
|
let cmd_name = cmd_line.next().unwrap();
|
|
|
|
|
let mut cmd = ChildCommand::new(cmd_name);
|
|
|
|
|
cmd.args(cmd_line);
|
|
|
|
|
|
|
|
|
|
match make_request(addr, &CliRequest::GetCredential { name, base })?? {
|
|
|
|
|
let req = CliRequest::GetCredential {
|
|
|
|
|
name: args.get_args.name,
|
|
|
|
|
base: args.get_args.base,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match make_request(global.server_addr, &req)?? {
|
|
|
|
|
CliResponse::Credential(CliCredential::AwsBase(creds)) => {
|
|
|
|
|
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
|
|
|
|
|
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
|
|
|
|
@ -133,11 +149,11 @@ pub fn exec(args: &ArgMatches, global_args: &ArgMatches) -> anyhow::Result<()> {
|
|
|
|
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
{
|
|
|
|
|
// cmd.exec() never returns if successful
|
|
|
|
|
let e = cmd.exec();
|
|
|
|
|
// cmd.exec() never returns if successful, so we never hit this line unless there's an error
|
|
|
|
|
Err(e).with_context(|| {
|
|
|
|
|
// eventually figure out how to display the actual command
|
|
|
|
|
format!("Failed to execute command")
|
|
|
|
|
format!("Failed to execute command: {}", args.command.join(" "))
|
|
|
|
|
})?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
@ -160,16 +176,9 @@ pub fn exec(args: &ArgMatches, global_args: &ArgMatches) -> anyhow::Result<()> {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn invoke_shortcut(args: &ArgMatches, global_args: &ArgMatches) -> anyhow::Result<()> {
|
|
|
|
|
let addr = global_args.get_one("server_addr").cloned();
|
|
|
|
|
let action = match args.get_one::<String>("action").map(|s| s.as_str()) {
|
|
|
|
|
Some("show_window") => ShortcutAction::ShowWindow,
|
|
|
|
|
Some("launch_terminal") => ShortcutAction::LaunchTerminal,
|
|
|
|
|
Some(&_) | None => unreachable!("Unknown shortcut action"), // guaranteed by clap
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let req = CliRequest::InvokeShortcut(action);
|
|
|
|
|
match make_request(addr, &req)?? {
|
|
|
|
|
pub fn invoke_shortcut(args: InvokeArgs, global: GlobalArgs) -> anyhow::Result<()> {
|
|
|
|
|
let req = CliRequest::InvokeShortcut(args.shortcut_action);
|
|
|
|
|
match make_request(global.server_addr, &req)?? {
|
|
|
|
|
CliResponse::Empty => Ok(()),
|
|
|
|
|
r => bail!("Unexpected response from server: {r}"),
|
|
|
|
|
}
|
|
|
|
|