use std::{
env,
path::{Path, PathBuf},
};
use clap::{Args, Parser, Subcommand, ValueEnum};
use eyre::{eyre, OptionExt};
use teller_core::{exec, export, teller::Teller};
use teller_providers::{config::KV, providers::ProviderKind};
use crate::{
io::{self, or_stdin, or_stdout},
new, scan, Response,
};
#[derive(Debug, Clone, Parser)] #[command(name = "teller")]
#[command(about = "A multi provider secret management tool", version, long_about = None)]
pub struct Cli {
#[arg(short, long)]
pub config: Option<String>,
#[arg(long)]
pub verbose: bool,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Debug, Clone, Subcommand)]
pub enum Commands {
Run {
#[arg(short, long)]
reset: bool,
#[arg(short, long)]
shell: bool,
#[arg(value_name = "COMMAND", raw = true)]
command: Vec<String>,
},
Scan(ScanArgs),
Export {
#[arg(value_enum, index = 1)]
format: Format,
},
Redact {
#[arg(name = "in", short, long)]
in_file: Option<String>,
#[arg(short, long)]
out: Option<String>,
},
Template {
#[arg(name = "in", short, long)]
in_file: Option<String>,
#[arg(short, long)]
out: Option<String>,
},
Env {},
Show {},
Sh {},
New(NewArgs),
Put {
#[arg(long, short)]
map_id: String,
#[arg(long, value_delimiter = ',')]
providers: Vec<String>,
#[clap(value_parser = parse_key_val::<String,String>)]
kvs: Vec<(String, String)>,
},
Delete {
#[arg(long, short)]
map_id: String,
#[arg(long, value_delimiter = ',')]
providers: Vec<String>,
keys: Vec<String>,
},
Copy {
#[arg(long, short)]
from: String,
#[arg(long, short, value_delimiter = ',')]
to: Vec<String>,
#[arg(long, short)]
replace: bool,
},
}
fn parse_key_val<T, U>(
s: &str,
) -> std::result::Result<(T, U), Box<dyn std::error::Error + Send + Sync>>
where
T: std::str::FromStr,
T::Err: std::error::Error + Send + Sync + 'static,
U: std::str::FromStr,
U::Err: std::error::Error + Send + Sync + 'static,
{
let pos = s
.find('=')
.ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum Format {
CSV,
YAML,
JSON,
ENV,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Args)] pub struct ScanArgs {
#[arg(short, long, default_value = ".")]
pub root: String,
#[arg(short, long)]
pub all: bool,
#[arg(long)]
pub error_if_found: bool,
#[arg(short, long)]
pub binary: bool,
#[arg(short, long)]
pub json: bool,
}
const DEFAULT_FILE_PATH: &str = ".teller.yml";
#[derive(Debug, Clone, Args)]
pub struct NewArgs {
#[arg(short, long, conflicts_with = "std", default_value=DEFAULT_FILE_PATH)]
pub filename: PathBuf,
#[arg(long)]
pub std: bool,
#[arg(long)]
pub force: bool,
#[arg(long, value_delimiter = ',')]
pub providers: Vec<ProviderKind>,
}
fn find_file_upwards(start_dir: &Path, config_filename: &str) -> eyre::Result<Option<PathBuf>> {
let mut current_dir = start_dir;
loop {
let config_path = current_dir.join(config_filename);
if config_path.exists() {
return Ok(Some(config_path));
}
match current_dir.parent() {
Some(parent) => current_dir = parent,
None => return Ok(None), }
}
}
async fn load_teller(config: Option<String>) -> eyre::Result<Teller> {
let config_arg = if let Some(config) = config {
config
} else {
find_file_upwards(env::current_dir()?.as_path(), DEFAULT_FILE_PATH)?
.ok_or_eyre("cannot find configuration from current folder and up to root")?
.to_string_lossy()
.to_string()
};
let config_path = Path::new(&config_arg);
let teller = Teller::from_yaml(config_path).await?;
Ok(teller)
}
#[allow(clippy::future_not_send)]
#[allow(clippy::too_many_lines)]
pub async fn run(args: &Cli) -> eyre::Result<Response> {
match args.command.clone() {
Commands::Run {
reset,
shell,
command,
} => {
let teller = load_teller(args.config.clone()).await?;
let pwd = std::env::current_dir()?;
let opts = exec::Opts {
pwd: pwd.as_path(),
sh: shell,
reset_env: reset,
capture: false,
};
teller
.run(
command
.iter()
.map(String::as_str)
.collect::<Vec<_>>()
.as_slice(),
&opts,
)
.await?;
Response::ok()
}
Commands::Scan(cmdargs) => {
let teller = load_teller(args.config.clone()).await?;
scan::run(&teller, &cmdargs).await
}
Commands::Export { format } => {
let teller_format = match format {
Format::CSV => export::Format::CSV,
Format::YAML => export::Format::YAML,
Format::JSON => export::Format::JSON,
Format::ENV => export::Format::ENV,
};
let teller = load_teller(args.config.clone()).await?;
let out = teller.export(&teller_format).await?;
Response::ok_with_message(out)
}
Commands::Redact { in_file, out } => {
let teller = load_teller(args.config.clone()).await?;
teller
.redact(&mut or_stdin(in_file)?, &mut or_stdout(out)?)
.await?;
Response::ok()
}
Commands::Template { in_file, out } => {
let mut input = String::new();
or_stdin(in_file)?.read_to_string(&mut input)?;
let teller = load_teller(args.config.clone()).await?;
let rendered = teller.template(&input).await?;
let mut out = or_stdout(out)?;
out.write_all(rendered.as_bytes())?;
out.flush()?;
Response::ok()
}
Commands::Env {} => {
let teller = load_teller(args.config.clone()).await?;
let out = teller.export(&export::Format::ENV).await?;
Response::ok_with_message(out)
}
Commands::New(new_args) => new::run(&new_args),
Commands::Show {} => {
let teller = load_teller(args.config.clone()).await?;
let kvs = teller.collect().await?;
io::print_kvs(&kvs);
Response::ok()
}
Commands::Sh {} => {
let teller = load_teller(args.config.clone()).await?;
let out = teller.export(&export::Format::Shell).await?;
Response::ok_with_message(out)
}
Commands::Put {
kvs,
map_id,
providers,
} => {
let kvs = kvs
.iter()
.map(|(k, v)| KV::from_kv(k, v))
.collect::<Vec<_>>();
let teller = load_teller(args.config.clone()).await?;
teller
.put(kvs.as_slice(), map_id.as_str(), providers.as_slice())
.await?;
Response::ok()
}
Commands::Delete {
map_id,
providers,
keys,
} => {
let teller = load_teller(args.config.clone()).await?;
teller
.delete(keys.as_slice(), &map_id, providers.as_slice())
.await?;
Response::ok()
}
Commands::Copy { from, to, replace } => {
let teller = load_teller(args.config.clone()).await?;
let (from_provider, from_map_id) = from.split_once('/').ok_or_else(|| {
eyre!(
"cannot parse '--from': '{}', did you format it as: '<provider name>/<map \
id>' ?",
from
)
})?;
for to_provider in to {
let (to_provider, to_map_id) = to_provider.split_once('/').ok_or_else(|| {
eyre!(
"cannot parse '--to': '{}', did you format it as: '<provider name>/<map \
id>' ?",
to_provider
)
})?;
teller
.copy(from_provider, from_map_id, to_provider, to_map_id, replace)
.await?;
}
Response::ok()
}
}
}