generated from pure_sagacity/rust-starter
558 lines
20 KiB
Rust
558 lines
20 KiB
Rust
use std::collections::HashMap;
|
|
use std::process;
|
|
|
|
use clap::Parser;
|
|
use clap::crate_version;
|
|
use cli::Cli;
|
|
use cli::Environment;
|
|
use cli::config::{Config, ConfigError};
|
|
use cli::gen_or_get_key;
|
|
use cli::get_input;
|
|
use cli::interactions::delete_secret;
|
|
use cli::interactions::get_project_id;
|
|
use cli::interactions::get_project_secrets;
|
|
use cli::interactions::get_projects;
|
|
use cli::interactions::set_secret;
|
|
use colored::*;
|
|
use crypto::encrypt;
|
|
|
|
const GIT_VERSION: &str = env!("GIT_VERSION");
|
|
|
|
fn main() {
|
|
use cli::{Commands, ProjectCommands};
|
|
let cli = Cli::parse();
|
|
|
|
if cli.version {
|
|
let top_msg = format!("Harbor version {}", crate_version!().green())
|
|
.bright_cyan()
|
|
.bold();
|
|
let bottom_msg =
|
|
format!("An open source secrets management and distribution platform.").blue();
|
|
let commit_msg = format!("Git commit {}", GIT_VERSION).dimmed();
|
|
|
|
println!("{}\n{}\n{}", top_msg, bottom_msg, commit_msg);
|
|
process::exit(0);
|
|
}
|
|
|
|
let root = match std::env::current_dir() {
|
|
Ok(path) => path,
|
|
Err(err) => {
|
|
eprintln!("Error resolving current directory: {}", err);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
let config_path = root.join(".harbor.toml");
|
|
let has_config = config_path.exists();
|
|
|
|
match cli.command {
|
|
Some(c) => match c {
|
|
_ if requires_config(&c) && !has_config => {
|
|
eprintln!(
|
|
"{}",
|
|
"Missing .harbor.toml. Run `harbor config create` first.".yellow()
|
|
);
|
|
process::exit(1);
|
|
}
|
|
Commands::Set { environment, vars } => {
|
|
let config = require_config(&root);
|
|
let environment: Environment = match environment {
|
|
Some(env) => match env.parse::<Environment>() {
|
|
Ok(parsed) => parsed,
|
|
Err(_) => {
|
|
eprintln!("Invalid environment '{}'.", env);
|
|
process::exit(1);
|
|
}
|
|
},
|
|
None => config.default_env.into(),
|
|
};
|
|
|
|
let pairs = match cli::parse_secret_pairs(&vars) {
|
|
Ok(pairs) => pairs,
|
|
Err(e) => {
|
|
eprintln!("Error parsing secrets: {}", e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
if pairs.is_empty() {
|
|
eprintln!("No secrets provided. Use KEY=VALUE pairs.");
|
|
process::exit(1);
|
|
}
|
|
|
|
let project = match get_project_id(&config.name) {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
eprintln!("Unable to get project ID for '{}': {}", config.name, e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
for pair in pairs {
|
|
let key = match gen_or_get_key() {
|
|
Ok(k) => k,
|
|
Err(_) => {
|
|
eprintln!("Error generating or getting key");
|
|
process::exit(1);
|
|
}
|
|
};
|
|
let (nonce, encrypted) = match encrypt(&key, pair.1.as_bytes().to_vec()) {
|
|
Ok(result) => result,
|
|
Err(e) => {
|
|
eprintln!("Error encrypting secret for key '{}': {}", pair.0, e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
match set_secret(&project, &pair.0, encrypted, environment, nonce) {
|
|
Ok(()) => {}
|
|
Err(e) => {
|
|
eprintln!("Error setting secret for key '{}': {}", pair.0, e);
|
|
process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Commands::Delete { keys } => {
|
|
let mut cleaned = Vec::new();
|
|
|
|
for key in keys {
|
|
let trimmed = key.trim();
|
|
if !trimmed.is_empty() {
|
|
cleaned.push(trimmed.to_string());
|
|
}
|
|
}
|
|
|
|
// Redefining as immutable
|
|
let cleaned = cleaned;
|
|
|
|
if cleaned.is_empty() {
|
|
eprintln!("No secret keys provided. Use one or more keys.");
|
|
process::exit(1);
|
|
}
|
|
|
|
for key in cleaned {
|
|
match delete_secret(&key) {
|
|
Ok(()) => {
|
|
println!("Deleted secret for key '{}'", key);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error deleting secret for key '{}': {}", key, e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
Commands::Inject { environment, after } => {
|
|
let config = require_config(&root);
|
|
let environment: Environment = match environment {
|
|
Some(env) => match env.parse::<Environment>() {
|
|
Ok(parsed) => parsed,
|
|
Err(_) => {
|
|
eprintln!("Invalid environment '{}'.", env);
|
|
process::exit(1);
|
|
}
|
|
},
|
|
None => config.default_env.into(),
|
|
};
|
|
let proj_id = match get_project_id(&config.name) {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
eprintln!("Unable to get project ID for '{}': {}", config.name, e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
// for harbor inject -- bun dev
|
|
// after = ["bun", "dev"]
|
|
|
|
let mut cmd = process::Command::new(&after[0]);
|
|
|
|
if after.len() > 1 {
|
|
cmd.args(&after[1..]);
|
|
}
|
|
|
|
let secrets = match get_project_secrets(&proj_id, environment) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
eprintln!("Error getting secrets: {}", e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
let mut uncrypted_envs: HashMap<String, String> = HashMap::new();
|
|
|
|
for (name, ciphertext, nonce) in secrets {
|
|
let key = match gen_or_get_key() {
|
|
Ok(k) => k,
|
|
Err(_) => {
|
|
eprintln!("Error generating or getting key");
|
|
process::exit(1);
|
|
}
|
|
};
|
|
let nonce = crypto::Nonce::from_slice(&nonce);
|
|
let decrypted = match crypto::decrypt(&key, ciphertext, nonce) {
|
|
Ok(d) => d,
|
|
Err(e) => {
|
|
eprintln!("Error decrypting secret for key '{}': {}", name, e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
let decrypted_str = match String::from_utf8(decrypted) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
eprintln!(
|
|
"Error converting decrypted secret to string for key '{}': {}",
|
|
name, e
|
|
);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
uncrypted_envs.insert(name, decrypted_str);
|
|
}
|
|
|
|
for (key, value) in uncrypted_envs {
|
|
cmd.env(key, value);
|
|
}
|
|
|
|
if cmd.status().is_err() {
|
|
eprintln!("Error waiting for command to finish: {:?}", after);
|
|
process::exit(1);
|
|
} else {
|
|
process::exit(0);
|
|
}
|
|
}
|
|
Commands::List {} => {
|
|
// Get all secrets, then use colorize and split them between environments, and print them out in a table format.
|
|
let config = require_config(&root);
|
|
let proj_id = match get_project_id(&config.name) {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
eprintln!("Unable to get project ID for '{}': {}", config.name, e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
let secrets = match get_project_secrets(&proj_id, Environment::Dev) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
eprintln!("Error getting secrets: {}", e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
if secrets.is_empty() {
|
|
println!("No secrets found for project '{}'.", config.name);
|
|
process::exit(0);
|
|
}
|
|
|
|
println!(
|
|
"{}",
|
|
format!("Secrets for project '{}':", config.name)
|
|
.bright_cyan()
|
|
.bold()
|
|
);
|
|
|
|
for (name, _, _) in secrets {
|
|
println!(" - {}", name.blue().bold());
|
|
}
|
|
}
|
|
Commands::Project { command } => match command {
|
|
ProjectCommands::List {} => {
|
|
let projects = match get_projects() {
|
|
Ok(projects) => projects,
|
|
Err(e) => {
|
|
eprintln!("Error getting projects: {}", e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
for project in projects {
|
|
println!(" - {}", project.name.blue().bold());
|
|
}
|
|
}
|
|
ProjectCommands::Create {} => {
|
|
let project_name = match get_input("Project name", ':', false) {
|
|
Ok(name) => name,
|
|
Err(e) => {
|
|
eprintln!("Error getting project name: {}", e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
match cli::interactions::create_project(&project_name) {
|
|
Ok(()) => println!("{}", "Project created successfully.".cyan()),
|
|
Err(e) => {
|
|
eprintln!("Error creating project: {}", e);
|
|
process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
ProjectCommands::Delete { name } => {
|
|
// We'll check if the project exists before prompting for confirmation
|
|
let exists = match cli::interactions::project_exists(&name) {
|
|
Ok(exists) => exists,
|
|
Err(e) => {
|
|
eprintln!("Error checking if project exists: {}", e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
if !exists {
|
|
eprintln!("Project '{}' does not exist.", name);
|
|
process::exit(1);
|
|
}
|
|
|
|
// Prompt for confirmation
|
|
let confirmation = match get_input(
|
|
format!(
|
|
"Are you sure you want to delete the project '{}'? (y/N)",
|
|
name
|
|
)
|
|
.red(),
|
|
':',
|
|
false,
|
|
) {
|
|
Ok(input) => input,
|
|
Err(e) => {
|
|
eprintln!("Error getting confirmation: {}", e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
if confirmation.to_lowercase() == "y" {
|
|
match cli::interactions::delete_project(&name) {
|
|
Ok(()) => {
|
|
println!("{}", "Project deleted successfully.".cyan())
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error deleting project: {}", e);
|
|
process::exit(1);
|
|
}
|
|
}
|
|
} else {
|
|
println!("Project deletion canceled.");
|
|
}
|
|
}
|
|
},
|
|
Commands::Shell {
|
|
environment,
|
|
shell,
|
|
command,
|
|
} => {
|
|
let config = require_config(&root);
|
|
let environment: Environment = match environment {
|
|
Some(env) => match env.parse::<Environment>() {
|
|
Ok(parsed) => parsed,
|
|
Err(_) => {
|
|
eprintln!("Invalid environment '{}'.", env);
|
|
process::exit(1);
|
|
}
|
|
},
|
|
None => config.default_env.into(),
|
|
};
|
|
let proj_id = match get_project_id(&config.name) {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
eprintln!("Unable to get project ID for '{}': {}", config.name, e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
// for harbor inject -- bun dev
|
|
// after = ["bun", "dev"]
|
|
|
|
let shell = shell
|
|
.or_else(|| std::env::var("SHELL").ok())
|
|
.or_else(|| Some("/bin/sh".into()))
|
|
.unwrap();
|
|
|
|
let mut cmd = process::Command::new(&shell);
|
|
|
|
if let Some(command) = command {
|
|
cmd.arg("-c");
|
|
cmd.arg(command);
|
|
}
|
|
|
|
let secrets = match get_project_secrets(&proj_id, environment) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
eprintln!("Error getting secrets: {}", e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
let mut uncrypted_envs: HashMap<String, String> = HashMap::new();
|
|
|
|
for (name, ciphertext, nonce) in secrets {
|
|
let key = match gen_or_get_key() {
|
|
Ok(k) => k,
|
|
Err(_) => {
|
|
eprintln!("Error generating or getting key");
|
|
process::exit(1);
|
|
}
|
|
};
|
|
let nonce = crypto::Nonce::from_slice(&nonce);
|
|
let decrypted = match crypto::decrypt(&key, ciphertext, nonce) {
|
|
Ok(d) => d,
|
|
Err(e) => {
|
|
eprintln!("Error decrypting secret for key '{}': {}", name, e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
let decrypted_str = match String::from_utf8(decrypted) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
eprintln!(
|
|
"Error converting decrypted secret to string for key '{}': {}",
|
|
name, e
|
|
);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
uncrypted_envs.insert(name, decrypted_str);
|
|
}
|
|
|
|
for (key, value) in uncrypted_envs {
|
|
cmd.env(key, value);
|
|
}
|
|
|
|
let mut child = match cmd.spawn() {
|
|
Ok(c) => c,
|
|
Err(_) => {
|
|
eprintln!("Error executing command: {:?}", &shell);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
if child.wait().is_err() {
|
|
eprintln!("Error waiting for command to finish: {:?}", &shell);
|
|
process::exit(1);
|
|
} else {
|
|
process::exit(0);
|
|
}
|
|
}
|
|
Commands::Setup {} => {
|
|
use std::process;
|
|
// Setup will get all projects and prompt the user to select one,
|
|
// then create a .harbor.toml file in the current directory with the selected project name.
|
|
let projects = match get_projects() {
|
|
Ok(projects) => projects,
|
|
Err(e) => {
|
|
eprintln!("Error getting projects: {}", e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
if projects.is_empty() {
|
|
eprintln!("No projects found. Please create a project first.");
|
|
process::exit(1);
|
|
}
|
|
|
|
// We'll just use fzf, and pipe the project names to it, and get the selected project name back.
|
|
let project_names: Vec<String> = projects.iter().map(|p| p.name.clone()).collect();
|
|
|
|
let mut fzf = match process::Command::new("fzf")
|
|
.stdin(process::Stdio::piped())
|
|
.stdout(process::Stdio::piped())
|
|
.spawn()
|
|
{
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
eprintln!("Error spawning fzf: {}", e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
{
|
|
let mut stdin = fzf.stdin.take().expect("Failed to open stdin");
|
|
for name in &project_names {
|
|
use std::io::Write;
|
|
writeln!(stdin, "{}", name).expect("Failed to write to stdin");
|
|
}
|
|
drop(stdin);
|
|
}
|
|
|
|
let output = match fzf.wait_with_output() {
|
|
Ok(o) => o,
|
|
Err(e) => {
|
|
eprintln!("Error waiting for fzf: {}", e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
if !output.status.success() {
|
|
eprintln!("fzf exited with non-zero status");
|
|
process::exit(1);
|
|
}
|
|
|
|
let project = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
|
|
if project.is_empty() {
|
|
eprintln!("No project selected.");
|
|
process::exit(1);
|
|
}
|
|
|
|
let new_config = format!(
|
|
r#"version = "1"
|
|
name = "{}"
|
|
config = "dev""#,
|
|
project
|
|
);
|
|
|
|
let config_path = root.join(".harbor.toml");
|
|
match std::fs::write(&config_path, new_config) {
|
|
Ok(_) => {
|
|
println!("Created .harbor.toml for project '{}'.", project);
|
|
}
|
|
Err(e) => {
|
|
eprintln!(
|
|
"Error creating .harbor.toml for project '{}': {}",
|
|
project, e
|
|
);
|
|
process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
None => {
|
|
println!(
|
|
"{}\n{}",
|
|
"No command was entered.".red(),
|
|
"Use --help for more information.".dimmed()
|
|
);
|
|
process::exit(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn require_config(root: &std::path::Path) -> Config {
|
|
match Config::from_repo_root(root) {
|
|
Ok(config) => config,
|
|
Err(ConfigError::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
|
|
eprintln!(
|
|
"{}",
|
|
"Missing .harbor.toml. Run `harbor config create` first.".yellow()
|
|
);
|
|
process::exit(1);
|
|
}
|
|
Err(err) => {
|
|
eprintln!("Error reading config: {}", err);
|
|
process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn requires_config(command: &cli::Commands) -> bool {
|
|
matches!(
|
|
command,
|
|
cli::Commands::Inject { .. }
|
|
| cli::Commands::Set { .. }
|
|
| cli::Commands::Delete { .. }
|
|
| cli::Commands::Shell { .. }
|
|
| cli::Commands::List { .. }
|
|
)
|
|
}
|