fixed tests, and added the deletion and overwriting confirmation

This commit is contained in:
2026-07-14 15:02:49 -05:00
parent 72ee5a918b
commit 767fb43ee2
3 changed files with 233 additions and 53 deletions
+43 -18
View File
@@ -125,9 +125,18 @@ pub enum Commands {
vars: Vec<String>, vars: Vec<String>,
}, },
Delete { Delete {
#[arg(short = 'e', long = "environment")]
environment: Option<String>,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)] #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
keys: Vec<String>, keys: Vec<String>,
}, },
Show {
#[arg(short = 'e', long = "environment")]
environment: Option<String>,
key: String,
},
Setup {}, Setup {},
List {}, List {},
Project { Project {
@@ -212,6 +221,10 @@ pub mod interactions {
type Result<T> = std::result::Result<T, Box<dyn Error>>; type Result<T> = std::result::Result<T, Box<dyn Error>>;
fn db_url() -> String {
std::env::var("DATABASE_URL").unwrap_or_else(|_| DB_URL.to_string())
}
fn construct_error(message: &str) -> Result<()> { fn construct_error(message: &str) -> Result<()> {
Err(Box::new(std::io::Error::new( Err(Box::new(std::io::Error::new(
std::io::ErrorKind::AlreadyExists, std::io::ErrorKind::AlreadyExists,
@@ -221,7 +234,7 @@ pub mod interactions {
pub fn project_exists(proj_name: &str) -> Result<bool> { pub fn project_exists(proj_name: &str) -> Result<bool> {
use super::db::schema::projects::dsl::{name, projects}; use super::db::schema::projects::dsl::{name, projects};
let mut conn = establish_connection(DB_URL.to_string()); let mut conn = establish_connection(db_url());
let existing_project = projects let existing_project = projects
.filter(name.eq(proj_name)) .filter(name.eq(proj_name))
@@ -234,7 +247,7 @@ pub mod interactions {
pub fn create_project(proj_name: &str) -> Result<()> { pub fn create_project(proj_name: &str) -> Result<()> {
use super::db::schema::projects::dsl::{created_at, id, name, projects}; use super::db::schema::projects::dsl::{created_at, id, name, projects};
let mut conn = establish_connection(DB_URL.to_string()); let mut conn = establish_connection(db_url());
if project_exists(proj_name)? { if project_exists(proj_name)? {
return construct_error("Project already exists"); return construct_error("Project already exists");
@@ -260,7 +273,7 @@ pub mod interactions {
pub fn delete_project(proj_name: &str) -> Result<()> { pub fn delete_project(proj_name: &str) -> Result<()> {
use super::db::schema::projects::dsl::{name, projects}; use super::db::schema::projects::dsl::{name, projects};
let mut conn = establish_connection(DB_URL.to_string()); let mut conn = establish_connection(db_url());
if !project_exists(proj_name)? { if !project_exists(proj_name)? {
return construct_error("Project does not exist"); return construct_error("Project does not exist");
@@ -273,7 +286,7 @@ pub mod interactions {
pub fn get_projects() -> Result<Vec<Project>> { pub fn get_projects() -> Result<Vec<Project>> {
use super::db::schema::projects::dsl::projects; use super::db::schema::projects::dsl::projects;
let mut conn = establish_connection(DB_URL.to_string()); let mut conn = establish_connection(db_url());
let results = projects let results = projects
.select(Project::as_select()) .select(Project::as_select())
@@ -288,7 +301,7 @@ pub mod interactions {
environment: Environment, environment: Environment,
) -> Result<bool> { ) -> Result<bool> {
use super::db::schema::secrets::dsl::{config, id, name, project_id, secrets}; use super::db::schema::secrets::dsl::{config, id, name, project_id, secrets};
let mut conn = establish_connection(DB_URL.to_string()); let mut conn = establish_connection(db_url());
let existing_secret = secrets let existing_secret = secrets
.filter(project_id.eq(proj_id)) .filter(project_id.eq(proj_id))
@@ -303,7 +316,7 @@ pub mod interactions {
pub fn get_project_id(proj_name: &str) -> Result<String> { pub fn get_project_id(proj_name: &str) -> Result<String> {
use super::db::schema::projects::dsl::{id, name, projects}; use super::db::schema::projects::dsl::{id, name, projects};
let mut conn = establish_connection(DB_URL.to_string()); let mut conn = establish_connection(db_url());
let project_id = projects let project_id = projects
.filter(name.eq(proj_name)) .filter(name.eq(proj_name))
@@ -330,15 +343,18 @@ pub mod interactions {
use super::db::schema::secrets::dsl::{ use super::db::schema::secrets::dsl::{
config, created_at, id, name, nonce, project_id, secret, secrets, config, created_at, id, name, nonce, project_id, secret, secrets,
}; };
let mut conn = establish_connection(DB_URL.to_string()); let mut conn = establish_connection(db_url());
if secret_exists(proj_id, secret_name, conf)? { if secret_exists(proj_id, secret_name, conf)? {
return Err(Box::new(std::io::Error::new( update(
std::io::ErrorKind::AlreadyExists, secrets
"Secret already exists", .filter(project_id.eq(proj_id))
))); .filter(name.eq(secret_name))
} .filter(config.eq(conf)),
)
.set((secret.eq(secret_value), nonce.eq(non.to_vec())))
.execute(&mut conn)?;
} else {
insert_into(secrets) insert_into(secrets)
.values(( .values((
id.eq(Uuid::new_v4().to_string()), id.eq(Uuid::new_v4().to_string()),
@@ -350,6 +366,7 @@ pub mod interactions {
created_at.eq(chrono::Utc::now().naive_utc()), created_at.eq(chrono::Utc::now().naive_utc()),
)) ))
.execute(&mut conn)?; .execute(&mut conn)?;
}
Ok(()) Ok(())
} }
@@ -359,7 +376,7 @@ pub mod interactions {
environment: Environment, environment: Environment,
) -> Result<Vec<(String, Vec<u8>, Vec<u8>)>> { ) -> Result<Vec<(String, Vec<u8>, Vec<u8>)>> {
use super::db::schema::secrets::dsl::{config, name, nonce, project_id, secret, secrets}; use super::db::schema::secrets::dsl::{config, name, nonce, project_id, secret, secrets};
let mut conn = establish_connection(DB_URL.to_string()); let mut conn = establish_connection(db_url());
let results = secrets let results = secrets
.filter(project_id.eq(proj_id)) .filter(project_id.eq(proj_id))
@@ -370,12 +387,14 @@ pub mod interactions {
Ok(results) Ok(results)
} }
pub fn delete_secret(secret_name: &str) -> Result<()> { pub fn delete_secret(proj_id: &str, secret_name: &str, environment: Environment) -> Result<()> {
use super::db::schema::secrets::dsl::{id, name, secrets}; use super::db::schema::secrets::dsl::{config, id, name, project_id, secrets};
let mut conn = establish_connection(DB_URL.to_string()); let mut conn = establish_connection(db_url());
let existing_secret = secrets let existing_secret = secrets
.filter(project_id.eq(proj_id))
.filter(name.eq(secret_name)) .filter(name.eq(secret_name))
.filter(config.eq(environment))
.select(id) .select(id)
.first::<String>(&mut conn) .first::<String>(&mut conn)
.optional()?; .optional()?;
@@ -387,7 +406,13 @@ pub mod interactions {
))); )));
} }
diesel::delete(secrets.filter(name.eq(secret_name))).execute(&mut conn)?; diesel::delete(
secrets
.filter(project_id.eq(proj_id))
.filter(name.eq(secret_name))
.filter(config.eq(environment)),
)
.execute(&mut conn)?;
Ok(()) Ok(())
} }
+167 -6
View File
@@ -12,6 +12,7 @@ use cli::interactions::delete_secret;
use cli::interactions::get_project_id; use cli::interactions::get_project_id;
use cli::interactions::get_project_secrets; use cli::interactions::get_project_secrets;
use cli::interactions::get_projects; use cli::interactions::get_projects;
use cli::interactions::secret_exists;
use cli::interactions::set_secret; use cli::interactions::set_secret;
use colored::*; use colored::*;
use crypto::encrypt; use crypto::encrypt;
@@ -55,6 +56,80 @@ fn main() {
print_error("Missing .harbor.toml. Run `harbor config create` first."); print_error("Missing .harbor.toml. Run `harbor config create` first.");
process::exit(1); process::exit(1);
} }
Commands::Show { environment, key } => {
let config = require_config(&root);
let environment: Environment = match environment {
Some(env) => match env.parse::<Environment>() {
Ok(parsed) => parsed,
Err(_) => {
print_error(format!("Invalid environment '{}'.", env));
process::exit(1);
}
},
None => config.default_env.into(),
};
let project = match get_project_id(&config.name) {
Ok(p) => p,
Err(e) => {
print_error(format!(
"Unable to get project ID for '{}': {}",
config.name, e
));
process::exit(1);
}
};
let secrets = match get_project_secrets(&project, environment) {
Ok(s) => s,
Err(e) => {
print_error(format!("Error getting secrets: {}", e));
process::exit(1);
}
};
let secret = secrets.into_iter().find(|(name, _, _)| name == &key);
match secret {
Some((_, ciphertext, nonce)) => {
let key = match gen_or_get_key() {
Ok(k) => k,
Err(_) => {
print_error("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) => {
print_error(format!("Error decrypting secret: {}", e));
process::exit(1);
}
};
let decrypted_str = match String::from_utf8(decrypted) {
Ok(s) => s,
Err(e) => {
print_error(format!(
"Error converting decrypted secret to string: {}",
e
));
process::exit(1);
}
};
println!("{}", decrypted_str);
}
None => {
print_error(format!(
"Secret with key '{}' not found in project '{}'.",
key, config.name
));
process::exit(1);
}
}
}
Commands::Set { environment, vars } => { Commands::Set { environment, vars } => {
let config = require_config(&root); let config = require_config(&root);
let environment: Environment = match environment { let environment: Environment = match environment {
@@ -93,6 +168,47 @@ fn main() {
}; };
for pair in pairs { for pair in pairs {
let existed = match secret_exists(&project, &pair.0, environment) {
Ok(exists) => exists,
Err(e) => {
print_error(format!(
"Error checking existing secret for key '{}': {}",
pair.0, e
));
process::exit(1);
}
};
if existed {
let confirmation = match get_input(
format!(
"Secret '{}' already exists. Overwrite? (y/N)",
pair.0
)
.yellow(),
':',
false,
) {
Ok(input) => input,
Err(e) => {
print_error(format!("Error getting confirmation: {}", e));
process::exit(1);
}
};
if confirmation.to_lowercase() != "y" {
println!(
"{}",
format!(
"Skipped secret '{}' in {}.",
pair.0,
environment.as_str()
)
.dimmed()
);
continue;
}
}
let key = match gen_or_get_key() { let key = match gen_or_get_key() {
Ok(k) => k, Ok(k) => k,
Err(_) => { Err(_) => {
@@ -111,7 +227,20 @@ fn main() {
} }
}; };
match set_secret(&project, &pair.0, encrypted, environment, nonce) { match set_secret(&project, &pair.0, encrypted, environment, nonce) {
Ok(()) => {} Ok(()) => {
let verb = if existed { "Updated" } else { "Set" };
println!(
"{}",
format!(
"{} secret '{}' in {}.",
verb,
pair.0,
environment.as_str()
)
.bright_green()
.bold()
);
}
Err(e) => { Err(e) => {
print_error(format!( print_error(format!(
"Error setting secret for key '{}': {}", "Error setting secret for key '{}': {}",
@@ -122,7 +251,28 @@ fn main() {
} }
} }
} }
Commands::Delete { keys } => { Commands::Delete { environment, keys } => {
let config = require_config(&root);
let environment: Environment = match environment {
Some(env) => match env.parse::<Environment>() {
Ok(parsed) => parsed,
Err(_) => {
print_error(format!("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) => {
print_error(format!(
"Unable to get project ID for '{}': {}",
config.name, e
));
process::exit(1);
}
};
let mut cleaned = Vec::new(); let mut cleaned = Vec::new();
for key in keys { for key in keys {
@@ -141,7 +291,7 @@ fn main() {
} }
for key in cleaned { for key in cleaned {
match delete_secret(&key) { match delete_secret(&proj_id, &key, environment) {
Ok(()) => { Ok(()) => {
println!("Deleted secret for key '{}'", key); println!("Deleted secret for key '{}'", key);
} }
@@ -300,7 +450,9 @@ fn main() {
}; };
match cli::interactions::create_project(&project_name) { match cli::interactions::create_project(&project_name) {
Ok(()) => println!("{}", "Project created successfully.".cyan()), Ok(()) => {
println!("{}", "Project created successfully.".bright_green().bold())
}
Err(e) => { Err(e) => {
print_error(format!("Error creating project: {}", e)); print_error(format!("Error creating project: {}", e));
process::exit(1); process::exit(1);
@@ -342,7 +494,10 @@ fn main() {
if confirmation.to_lowercase() == "y" { if confirmation.to_lowercase() == "y" {
match cli::interactions::delete_project(&name) { match cli::interactions::delete_project(&name) {
Ok(()) => { Ok(()) => {
println!("{}", "Project deleted successfully.".cyan()) println!(
"{}",
"Project deleted successfully.".bright_green().bold()
)
} }
Err(e) => { Err(e) => {
print_error(format!("Error deleting project: {}", e)); print_error(format!("Error deleting project: {}", e));
@@ -530,7 +685,12 @@ config = "dev""#,
let config_path = root.join(".harbor.toml"); let config_path = root.join(".harbor.toml");
match std::fs::write(&config_path, new_config) { match std::fs::write(&config_path, new_config) {
Ok(_) => { Ok(_) => {
println!("Created .harbor.toml for project '{}'.", project); println!(
"{}",
format!("Created .harbor.toml for project '{}'.", project)
.bright_green()
.bold()
);
} }
Err(e) => { Err(e) => {
print_error(format!( print_error(format!(
@@ -571,6 +731,7 @@ fn requires_config(command: &cli::Commands) -> bool {
matches!( matches!(
command, command,
cli::Commands::Inject { .. } cli::Commands::Inject { .. }
| cli::Commands::Show { .. }
| cli::Commands::Set { .. } | cli::Commands::Set { .. }
| cli::Commands::Delete { .. } | cli::Commands::Delete { .. }
| cli::Commands::Shell { .. } | cli::Commands::Shell { .. }
+13 -19
View File
@@ -1,15 +1,8 @@
use cli::Environment; use cli::Environment;
use cli::config::Config; use cli::config::Config;
use cli::interactions::{ use cli::interactions::{
create_project, create_project, delete_project, delete_secret, get_project_id, get_project_secrets,
delete_project, get_projects, project_exists, secret_exists, set_secret,
delete_secret,
get_project_id,
get_project_secrets,
get_projects,
project_exists,
secret_exists,
set_secret,
}; };
use cli::{format_doppler_set_command, parse_secret_pairs}; use cli::{format_doppler_set_command, parse_secret_pairs};
use diesel::connection::SimpleConnection; use diesel::connection::SimpleConnection;
@@ -70,7 +63,10 @@ fn environment_parsing_and_display() {
assert_eq!(format!("{}", Environment::Prod), "prod"); assert_eq!(format!("{}", Environment::Prod), "prod");
let err = "unknown".parse::<Environment>().unwrap_err(); let err = "unknown".parse::<Environment>().unwrap_err();
assert_eq!(err.to_string(), "Environment must be one of: dev, prod, staging"); assert_eq!(
err.to_string(),
"Environment must be one of: dev, prod, staging"
);
} }
#[test] #[test]
@@ -108,10 +104,7 @@ fn format_doppler_set_command_outputs_expected_format() {
("B".to_string(), "2".to_string()), ("B".to_string(), "2".to_string()),
]); ]);
assert_eq!( assert_eq!(many, "doppler secrets set \\\n A=\"1\" \\\n B=\"2\"");
many,
"doppler secrets set \\\n A=\"1\" \\\n B=\"2\""
);
} }
#[test] #[test]
@@ -158,7 +151,7 @@ fn project_and_secret_lifecycle() {
assert_eq!(projects.len(), 1); assert_eq!(projects.len(), 1);
assert_eq!(projects[0].name, "alpha"); assert_eq!(projects[0].name, "alpha");
assert!(!secret_exists().expect("secret exists")); assert!(!secret_exists(&project_id, "API_KEY", Environment::Dev).expect("secret exists"));
let nonce = crypto::helper::gen_nonce(); let nonce = crypto::helper::gen_nonce();
set_secret( set_secret(
@@ -170,7 +163,7 @@ fn project_and_secret_lifecycle() {
) )
.expect("set secret"); .expect("set secret");
assert!(secret_exists().expect("secret exists")); assert!(secret_exists(&project_id, "API_KEY", Environment::Dev).expect("secret exists"));
let secrets = get_project_secrets(&project_id, Environment::Dev).expect("get secrets"); let secrets = get_project_secrets(&project_id, Environment::Dev).expect("get secrets");
assert_eq!(secrets.len(), 1); assert_eq!(secrets.len(), 1);
@@ -191,8 +184,8 @@ fn project_and_secret_lifecycle() {
assert_eq!(updated.len(), 1); assert_eq!(updated.len(), 1);
assert_eq!(updated[0].1, b"second".to_vec()); assert_eq!(updated[0].1, b"second".to_vec());
delete_secret("API_KEY").expect("delete secret"); delete_secret(&project_id, "API_KEY", Environment::Dev).expect("delete secret");
assert!(!secret_exists().expect("secret exists")); assert!(!secret_exists(&project_id, "API_KEY", Environment::Dev).expect("secret exists"));
delete_project("alpha").expect("delete project"); delete_project("alpha").expect("delete project");
assert!(!project_exists("alpha").expect("project exists")); assert!(!project_exists("alpha").expect("project exists"));
@@ -211,7 +204,8 @@ fn errors_for_missing_project_or_secret() {
let err = get_project_id("missing").unwrap_err(); let err = get_project_id("missing").unwrap_err();
assert_eq!(err.to_string(), "Project not found"); assert_eq!(err.to_string(), "Project not found");
let err = delete_secret("missing").unwrap_err(); let missing_project_id = Uuid::new_v4().to_string();
let err = delete_secret(&missing_project_id, "missing", Environment::Dev).unwrap_err();
assert_eq!(err.to_string(), "Secret does not exist"); assert_eq!(err.to_string(), "Secret does not exist");
cleanup_test_db(db_path); cleanup_test_db(db_path);