switched to using the Store trait

also added another dependency for expanding ~ paths
This commit is contained in:
2026-07-16 11:23:35 -05:00
parent 767fb43ee2
commit 5d5b0ae89a
7 changed files with 398 additions and 325 deletions
Generated
+48
View File
@@ -448,6 +448,7 @@ dependencies = [
"colored", "colored",
"crypto", "crypto",
"diesel", "diesel",
"dirs",
"dotenvy", "dotenvy",
"hex", "hex",
"keyring", "keyring",
@@ -653,6 +654,27 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys",
]
[[package]] [[package]]
name = "dotenvy" name = "dotenvy"
version = "0.15.7" version = "0.15.7"
@@ -1024,6 +1046,15 @@ version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libredox"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
version = "0.37.0" version = "0.37.0"
@@ -1171,6 +1202,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]] [[package]]
name = "ordered-stream" name = "ordered-stream"
version = "0.2.0" version = "0.2.0"
@@ -1322,6 +1359,17 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "redox_users"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom 0.2.17",
"libredox",
"thiserror",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.13.0" version = "1.13.0"
+4 -3
View File
@@ -13,17 +13,18 @@ diesel = { version = "2.2.0", features = [
"chrono", "chrono",
] } ] }
clap = { version = "4.6.1", features = ["derive", "cargo"] } clap = { version = "4.6.1", features = ["derive", "cargo"] }
serde = { version = "1.0.228", features = ["derive"] }
tokio = { version = "1.52.3", features = ["full"] } tokio = { version = "1.52.3", features = ["full"] }
uuid = { version = "1.23.5", features = ["v4"] }
crypto = { path = "../crypto" } crypto = { path = "../crypto" }
keyring = "4.1.4" keyring = "4.1.4"
colored = "3.1.1" colored = "3.1.1"
base64 = "0.22.1" base64 = "0.22.1"
chrono = "0.4.45" chrono = "0.4.45"
dotenvy = "0.15" dotenvy = "0.15"
uuid = { version = "1.23.5", features = ["v4"] }
hex = "0.4"
serde = { version = "1.0.228", features = ["derive"] }
toml = "1.1.2" toml = "1.1.2"
hex = "0.4"
dirs = "6"
[[bin]] [[bin]]
name = "harbor" name = "harbor"
+7 -231
View File
@@ -10,9 +10,10 @@ use keyring::Entry;
use serde::Deserialize; use serde::Deserialize;
use std::error::Error; use std::error::Error;
use std::io::{self, Write}; use std::io::{self, Write};
use std::path::PathBuf;
pub mod config; pub mod config;
mod db; mod db;
mod store; pub mod store;
#[derive( #[derive(
Debug, Clone, Copy, PartialEq, Eq, Deserialize, diesel::AsExpression, diesel::FromSqlRow, Debug, Clone, Copy, PartialEq, Eq, Deserialize, diesel::AsExpression, diesel::FromSqlRow,
@@ -175,30 +176,6 @@ pub fn parse_secret_pairs(raw: &[String]) -> Result<Vec<(String, String)>, Box<d
Ok(pairs) Ok(pairs)
} }
pub fn format_doppler_set_command(secrets: &[(String, String)]) -> String {
let mut command = String::from("doppler secrets set");
if secrets.is_empty() {
return command;
}
command.push_str(" \\");
for (index, (key, value)) in secrets.iter().enumerate() {
command.push_str("\n ");
command.push_str(key);
command.push_str("=\"");
command.push_str(value);
command.push('"');
if index + 1 != secrets.len() {
command.push_str(" \\");
}
}
command
}
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum ProjectCommands { pub enum ProjectCommands {
List {}, List {},
@@ -206,217 +183,16 @@ pub enum ProjectCommands {
Delete { name: String }, Delete { name: String },
} }
const DB_URL: &str = "/Users/Maaz/Documents/Git/harbor/crates/cli/harbor.db"; pub fn expand_tilde(path: &str) -> String {
if let Some(stripped) = path.strip_prefix("~/") {
pub mod interactions { if let Some(home) = dirs::home_dir() {
use super::DB_URL; return home.join(stripped).to_string_lossy().to_string();
use super::db::{establish_connection, models::Project};
use crate::Environment;
use crate::db::schema::projects::id;
use diesel::dsl::{insert_into, update};
use diesel::result::{DatabaseErrorKind, Error as DieselError};
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper};
use std::error::Error;
use uuid::Uuid;
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<()> {
Err(Box::new(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
message.to_string(),
)))
}
pub fn project_exists(proj_name: &str) -> Result<bool> {
use super::db::schema::projects::dsl::{name, projects};
let mut conn = establish_connection(db_url());
let existing_project = projects
.filter(name.eq(proj_name))
.select(Project::as_select())
.first::<Project>(&mut conn)
.optional()?;
Ok(existing_project.is_some())
}
pub fn create_project(proj_name: &str) -> Result<()> {
use super::db::schema::projects::dsl::{created_at, id, name, projects};
let mut conn = establish_connection(db_url());
if project_exists(proj_name)? {
return construct_error("Project already exists");
}
let proj_id = Uuid::new_v4().to_string();
match insert_into(projects)
.values((
id.eq(proj_id),
name.eq(proj_name),
created_at.eq(chrono::Utc::now().naive_utc()),
))
.execute(&mut conn)
{
Ok(_) => Ok(()),
Err(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) => {
construct_error("Project already exists")
}
Err(err) => Err(Box::new(err)),
} }
} }
pub fn delete_project(proj_name: &str) -> Result<()> { path.to_string()
use super::db::schema::projects::dsl::{name, projects};
let mut conn = establish_connection(db_url());
if !project_exists(proj_name)? {
return construct_error("Project does not exist");
} }
diesel::delete(projects.filter(name.eq(proj_name))).execute(&mut conn)?;
Ok(())
}
pub fn get_projects() -> Result<Vec<Project>> {
use super::db::schema::projects::dsl::projects;
let mut conn = establish_connection(db_url());
let results = projects
.select(Project::as_select())
.load::<Project>(&mut conn)?;
Ok(results)
}
pub fn secret_exists(
proj_id: &str,
secret_name: &str,
environment: Environment,
) -> Result<bool> {
use super::db::schema::secrets::dsl::{config, id, name, project_id, secrets};
let mut conn = establish_connection(db_url());
let existing_secret = secrets
.filter(project_id.eq(proj_id))
.filter(name.eq(secret_name))
.filter(config.eq(environment))
.select(id)
.first::<String>(&mut conn)
.optional()?;
Ok(existing_secret.is_some())
}
pub fn get_project_id(proj_name: &str) -> Result<String> {
use super::db::schema::projects::dsl::{id, name, projects};
let mut conn = establish_connection(db_url());
let project_id = projects
.filter(name.eq(proj_name))
.select(id)
.first::<String>(&mut conn)
.optional()?;
match project_id {
Some(pid) => Ok(pid),
None => Err(Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Project not found",
))),
}
}
pub fn set_secret(
proj_id: &str,
secret_name: &str,
secret_value: Vec<u8>,
conf: Environment,
non: crypto::Nonce,
) -> Result<()> {
use super::db::schema::secrets::dsl::{
config, created_at, id, name, nonce, project_id, secret, secrets,
};
let mut conn = establish_connection(db_url());
if secret_exists(proj_id, secret_name, conf)? {
update(
secrets
.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)
.values((
id.eq(Uuid::new_v4().to_string()),
name.eq(secret_name),
secret.eq(secret_value),
project_id.eq(proj_id),
config.eq(conf),
nonce.eq(non.to_vec()),
created_at.eq(chrono::Utc::now().naive_utc()),
))
.execute(&mut conn)?;
}
Ok(())
}
pub fn get_project_secrets(
proj_id: &str,
environment: Environment,
) -> Result<Vec<(String, Vec<u8>, Vec<u8>)>> {
use super::db::schema::secrets::dsl::{config, name, nonce, project_id, secret, secrets};
let mut conn = establish_connection(db_url());
let results = secrets
.filter(project_id.eq(proj_id))
.filter(config.eq(environment))
.select((name, secret, nonce))
.load::<(String, Vec<u8>, Vec<u8>)>(&mut conn)?;
Ok(results)
}
pub fn delete_secret(proj_id: &str, secret_name: &str, environment: Environment) -> Result<()> {
use super::db::schema::secrets::dsl::{config, id, name, project_id, secrets};
let mut conn = establish_connection(db_url());
let existing_secret = secrets
.filter(project_id.eq(proj_id))
.filter(name.eq(secret_name))
.filter(config.eq(environment))
.select(id)
.first::<String>(&mut conn)
.optional()?;
if existing_secret.is_none() {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Secret does not exist",
)));
}
diesel::delete(
secrets
.filter(project_id.eq(proj_id))
.filter(name.eq(secret_name))
.filter(config.eq(environment)),
)
.execute(&mut conn)?;
Ok(())
}
}
pub fn gen_or_get_key() -> Result<crypto::Key, Box<dyn Error>> { pub fn gen_or_get_key() -> Result<crypto::Key, Box<dyn Error>> {
let entry = Entry::new("harbor", "encryption-key")?; let entry = Entry::new("harbor", "encryption-key")?;
+25 -33
View File
@@ -6,14 +6,11 @@ use clap::crate_version;
use cli::Cli; use cli::Cli;
use cli::Environment; use cli::Environment;
use cli::config::{Config, ConfigError}; use cli::config::{Config, ConfigError};
use cli::expand_tilde;
use cli::gen_or_get_key; use cli::gen_or_get_key;
use cli::get_input; use cli::get_input;
use cli::interactions::delete_secret; use cli::store::Store;
use cli::interactions::get_project_id; use cli::store::sqlite::SqliteStore;
use cli::interactions::get_project_secrets;
use cli::interactions::get_projects;
use cli::interactions::secret_exists;
use cli::interactions::set_secret;
use colored::*; use colored::*;
use crypto::encrypt; use crypto::encrypt;
@@ -47,8 +44,10 @@ fn main() {
} }
}; };
let db_path = expand_tilde("~/.local/share/harbor/database.db");
let config_path = root.join(".harbor.toml"); let config_path = root.join(".harbor.toml");
let has_config = config_path.exists(); let has_config = config_path.exists();
let store = SqliteStore::new(db_path);
match cli.command { match cli.command {
Some(c) => match c { Some(c) => match c {
@@ -69,7 +68,7 @@ fn main() {
None => config.default_env.into(), None => config.default_env.into(),
}; };
let project = match get_project_id(&config.name) { let project = match store.get_project_id(&config.name) {
Ok(p) => p, Ok(p) => p,
Err(e) => { Err(e) => {
print_error(format!( print_error(format!(
@@ -80,7 +79,7 @@ fn main() {
} }
}; };
let secrets = match get_project_secrets(&project, environment) { let secrets = match store.get_project_secrets(&project, environment) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
print_error(format!("Error getting secrets: {}", e)); print_error(format!("Error getting secrets: {}", e));
@@ -156,7 +155,7 @@ fn main() {
process::exit(1); process::exit(1);
} }
let project = match get_project_id(&config.name) { let project = match store.get_project_id(&config.name) {
Ok(p) => p, Ok(p) => p,
Err(e) => { Err(e) => {
print_error(format!( print_error(format!(
@@ -168,7 +167,7 @@ fn main() {
}; };
for pair in pairs { for pair in pairs {
let existed = match secret_exists(&project, &pair.0, environment) { let existed = match store.secret_exists(&project, &pair.0, environment) {
Ok(exists) => exists, Ok(exists) => exists,
Err(e) => { Err(e) => {
print_error(format!( print_error(format!(
@@ -180,10 +179,7 @@ fn main() {
}; };
if existed { if existed {
let confirmation = match get_input( let confirmation = match get_input(
format!( format!("Secret '{}' already exists. Overwrite? (y/N)", pair.0)
"Secret '{}' already exists. Overwrite? (y/N)",
pair.0
)
.yellow(), .yellow(),
':', ':',
false, false,
@@ -198,11 +194,7 @@ fn main() {
if confirmation.to_lowercase() != "y" { if confirmation.to_lowercase() != "y" {
println!( println!(
"{}", "{}",
format!( format!("Skipped secret '{}' in {}.", pair.0, environment.as_str())
"Skipped secret '{}' in {}.",
pair.0,
environment.as_str()
)
.dimmed() .dimmed()
); );
continue; continue;
@@ -226,7 +218,7 @@ fn main() {
process::exit(1); process::exit(1);
} }
}; };
match set_secret(&project, &pair.0, encrypted, environment, nonce) { match store.set_secret(&project, &pair.0, encrypted, environment, nonce) {
Ok(()) => { Ok(()) => {
let verb = if existed { "Updated" } else { "Set" }; let verb = if existed { "Updated" } else { "Set" };
println!( println!(
@@ -263,7 +255,7 @@ fn main() {
}, },
None => config.default_env.into(), None => config.default_env.into(),
}; };
let proj_id = match get_project_id(&config.name) { let proj_id = match store.get_project_id(&config.name) {
Ok(p) => p, Ok(p) => p,
Err(e) => { Err(e) => {
print_error(format!( print_error(format!(
@@ -291,7 +283,7 @@ fn main() {
} }
for key in cleaned { for key in cleaned {
match delete_secret(&proj_id, &key, environment) { match store.delete_secret(&proj_id, &key, environment) {
Ok(()) => { Ok(()) => {
println!("Deleted secret for key '{}'", key); println!("Deleted secret for key '{}'", key);
} }
@@ -314,7 +306,7 @@ fn main() {
}, },
None => config.default_env.into(), None => config.default_env.into(),
}; };
let proj_id = match get_project_id(&config.name) { let proj_id = match store.get_project_id(&config.name) {
Ok(p) => p, Ok(p) => p,
Err(e) => { Err(e) => {
print_error(format!( print_error(format!(
@@ -334,7 +326,7 @@ fn main() {
cmd.args(&after[1..]); cmd.args(&after[1..]);
} }
let secrets = match get_project_secrets(&proj_id, environment) { let secrets = match store.get_project_secrets(&proj_id, environment) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
print_error(format!("Error getting secrets: {}", e)); print_error(format!("Error getting secrets: {}", e));
@@ -392,7 +384,7 @@ fn main() {
Commands::List {} => { Commands::List {} => {
// Get all secrets, then use colorize and split them between environments, and print them out in a table format. // 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 config = require_config(&root);
let proj_id = match get_project_id(&config.name) { let proj_id = match store.get_project_id(&config.name) {
Ok(p) => p, Ok(p) => p,
Err(e) => { Err(e) => {
print_error(format!( print_error(format!(
@@ -403,7 +395,7 @@ fn main() {
} }
}; };
let secrets = match get_project_secrets(&proj_id, Environment::Dev) { let secrets = match store.get_project_secrets(&proj_id, Environment::Dev) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
print_error(format!("Error getting secrets: {}", e)); print_error(format!("Error getting secrets: {}", e));
@@ -429,7 +421,7 @@ fn main() {
} }
Commands::Project { command } => match command { Commands::Project { command } => match command {
ProjectCommands::List {} => { ProjectCommands::List {} => {
let projects = match get_projects() { let projects = match store.get_projects() {
Ok(projects) => projects, Ok(projects) => projects,
Err(e) => { Err(e) => {
print_error(format!("Error getting projects: {}", e)); print_error(format!("Error getting projects: {}", e));
@@ -449,7 +441,7 @@ fn main() {
} }
}; };
match cli::interactions::create_project(&project_name) { match store.create_project(&project_name) {
Ok(()) => { Ok(()) => {
println!("{}", "Project created successfully.".bright_green().bold()) println!("{}", "Project created successfully.".bright_green().bold())
} }
@@ -461,7 +453,7 @@ fn main() {
} }
ProjectCommands::Delete { name } => { ProjectCommands::Delete { name } => {
// We'll check if the project exists before prompting for confirmation // We'll check if the project exists before prompting for confirmation
let exists = match cli::interactions::project_exists(&name) { let exists = match store.project_exists(&name) {
Ok(exists) => exists, Ok(exists) => exists,
Err(e) => { Err(e) => {
print_error(format!("Error checking if project exists: {}", e)); print_error(format!("Error checking if project exists: {}", e));
@@ -492,7 +484,7 @@ fn main() {
}; };
if confirmation.to_lowercase() == "y" { if confirmation.to_lowercase() == "y" {
match cli::interactions::delete_project(&name) { match store.delete_project(&name) {
Ok(()) => { Ok(()) => {
println!( println!(
"{}", "{}",
@@ -525,7 +517,7 @@ fn main() {
}, },
None => config.default_env.into(), None => config.default_env.into(),
}; };
let proj_id = match get_project_id(&config.name) { let proj_id = match store.get_project_id(&config.name) {
Ok(p) => p, Ok(p) => p,
Err(e) => { Err(e) => {
print_error(format!( print_error(format!(
@@ -551,7 +543,7 @@ fn main() {
cmd.arg(command); cmd.arg(command);
} }
let secrets = match get_project_secrets(&proj_id, environment) { let secrets = match store.get_project_secrets(&proj_id, environment) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
print_error(format!("Error getting secrets: {}", e)); print_error(format!("Error getting secrets: {}", e));
@@ -618,7 +610,7 @@ fn main() {
use std::process; use std::process;
// Setup will get all projects and prompt the user to select one, // 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. // then create a .harbor.toml file in the current directory with the selected project name.
let projects = match get_projects() { let projects = match store.get_projects() {
Ok(projects) => projects, Ok(projects) => projects,
Err(e) => { Err(e) => {
print_error(format!("Error getting projects: {}", e)); print_error(format!("Error getting projects: {}", e));
+32 -2
View File
@@ -3,5 +3,35 @@
// With this trait, we can implement other savers like Postgres, MySQL, etc. // With this trait, we can implement other savers like Postgres, MySQL, etc.
// Or even third party services like Hashicorp Vault, AWS Secrets Manager, etc. // Or even third party services like Hashicorp Vault, AWS Secrets Manager, etc.
// For now, I'll just get an MVP in place. use std::error::Error;
pub trait Store {}
pub mod sqlite;
use crate::{Environment, db::models::Project};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
pub trait Store {
fn project_exists(&self, proj_name: &str) -> Result<bool>;
fn create_project(&self, proj_name: &str) -> Result<()>;
fn delete_project(&self, proj_name: &str) -> Result<()>;
fn get_projects(&self) -> Result<Vec<Project>>;
fn get_project_id(&self, proj_name: &str) -> Result<String>;
fn secret_exists(&self, proj_id: &str, secret_name: &str, environment: Environment)
-> Result<bool>;
fn set_secret(
&self,
proj_id: &str,
secret_name: &str,
secret_value: Vec<u8>,
conf: Environment,
non: crypto::Nonce,
) -> Result<()>;
fn get_project_secrets(
&self,
proj_id: &str,
environment: Environment,
) -> Result<Vec<(String, Vec<u8>, Vec<u8>)>>;
fn delete_secret(&self, proj_id: &str, secret_name: &str, environment: Environment)
-> Result<()>;
}
+220
View File
@@ -0,0 +1,220 @@
use crate::db::models::Project;
use crate::store::Store;
use diesel::dsl::{insert_into, update};
use diesel::result::{DatabaseErrorKind, Error as DieselError};
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper};
use uuid::Uuid;
pub struct SqliteStore {
database_url: String,
}
impl SqliteStore {
pub fn new(database_url: String) -> Self {
SqliteStore { database_url }
}
fn db_url(&self) -> String {
if self.database_url.is_empty() {
std::env::var("DATABASE_URL").expect("NO DATABASE URL WAS SET")
} else {
self.database_url.clone()
}
}
}
fn construct_error(message: &str) -> super::Result<()> {
Err(Box::new(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
message.to_string(),
)))
}
impl Store for SqliteStore {
fn create_project(&self, proj_name: &str) -> super::Result<()> {
use crate::db::schema::projects::dsl::{created_at, id, name, projects};
let mut conn = crate::db::establish_connection(self.db_url());
if self.project_exists(proj_name)? {
return construct_error("Project already exists");
}
let proj_id = Uuid::new_v4().to_string();
match insert_into(projects)
.values((
id.eq(proj_id),
name.eq(proj_name),
created_at.eq(chrono::Utc::now().naive_utc()),
))
.execute(&mut conn)
{
Ok(_) => Ok(()),
Err(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) => {
construct_error("Project already exists")
}
Err(err) => Err(Box::new(err)),
}
}
fn delete_project(&self, proj_name: &str) -> super::Result<()> {
use crate::db::schema::projects::dsl::{name, projects};
let mut conn = crate::db::establish_connection(self.db_url());
if !self.project_exists(proj_name)? {
return construct_error("Project does not exist");
}
diesel::delete(projects.filter(name.eq(proj_name))).execute(&mut conn)?;
Ok(())
}
fn delete_secret(
&self,
proj_id: &str,
secret_name: &str,
environment: crate::Environment,
) -> super::Result<()> {
use crate::db::schema::secrets::dsl::{config, id, name, project_id, secrets};
let mut conn = crate::db::establish_connection(self.db_url());
let existing_secret = secrets
.filter(project_id.eq(proj_id))
.filter(name.eq(secret_name))
.filter(config.eq(environment))
.select(id)
.first::<String>(&mut conn)
.optional()?;
if existing_secret.is_none() {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Secret does not exist",
)));
}
diesel::delete(
secrets
.filter(project_id.eq(proj_id))
.filter(name.eq(secret_name))
.filter(config.eq(environment)),
)
.execute(&mut conn)?;
Ok(())
}
fn get_project_id(&self, proj_name: &str) -> super::Result<String> {
use crate::db::schema::projects::dsl::{id, name, projects};
let mut conn = crate::db::establish_connection(self.db_url());
let project_id = projects
.filter(name.eq(proj_name))
.select(id)
.first::<String>(&mut conn)
.optional()?;
match project_id {
Some(pid) => Ok(pid),
None => Err(Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Project not found",
))),
}
}
fn get_project_secrets(
&self,
proj_id: &str,
environment: crate::Environment,
) -> super::Result<Vec<(String, Vec<u8>, Vec<u8>)>> {
use crate::db::schema::secrets::dsl::{config, name, nonce, project_id, secret, secrets};
let mut conn = crate::db::establish_connection(self.db_url());
let results = secrets
.filter(project_id.eq(proj_id))
.filter(config.eq(environment))
.select((name, secret, nonce))
.load::<(String, Vec<u8>, Vec<u8>)>(&mut conn)?;
Ok(results)
}
fn get_projects(&self) -> super::Result<Vec<Project>> {
use crate::db::schema::projects::dsl::projects;
let mut conn = crate::db::establish_connection(self.db_url());
let results = projects
.select(Project::as_select())
.load::<Project>(&mut conn)?;
Ok(results)
}
fn project_exists(&self, proj_name: &str) -> super::Result<bool> {
use crate::db::schema::projects::dsl::{name, projects};
let mut conn = crate::db::establish_connection(self.db_url());
let existing_project = projects
.filter(name.eq(proj_name))
.select(Project::as_select())
.first::<Project>(&mut conn)
.optional()?;
Ok(existing_project.is_some())
}
fn secret_exists(
&self,
proj_id: &str,
secret_name: &str,
environment: crate::Environment,
) -> super::Result<bool> {
use crate::db::schema::secrets::dsl::{config, id, name, project_id, secrets};
let mut conn = crate::db::establish_connection(self.db_url());
let existing_secret = secrets
.filter(project_id.eq(proj_id))
.filter(name.eq(secret_name))
.filter(config.eq(environment))
.select(id)
.first::<String>(&mut conn)
.optional()?;
Ok(existing_secret.is_some())
}
fn set_secret(
&self,
proj_id: &str,
secret_name: &str,
secret_value: Vec<u8>,
conf: crate::Environment,
non: crypto::Nonce,
) -> super::Result<()> {
use crate::db::schema::secrets::dsl::{
config, created_at, id, name, nonce, project_id, secret, secrets,
};
let mut conn = crate::db::establish_connection(self.db_url());
if self.secret_exists(proj_id, secret_name, conf)? {
update(
secrets
.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)
.values((
id.eq(Uuid::new_v4().to_string()),
name.eq(secret_name),
secret.eq(secret_value),
project_id.eq(proj_id),
config.eq(conf),
nonce.eq(non.to_vec()),
created_at.eq(chrono::Utc::now().naive_utc()),
))
.execute(&mut conn)?;
}
Ok(())
}
}
+45 -39
View File
@@ -1,10 +1,8 @@
use cli::Environment; use cli::Environment;
use cli::config::Config; use cli::config::Config;
use cli::interactions::{ use cli::parse_secret_pairs;
create_project, delete_project, delete_secret, get_project_id, get_project_secrets, use cli::store::Store;
get_projects, project_exists, secret_exists, set_secret, use cli::store::sqlite::SqliteStore;
};
use cli::{format_doppler_set_command, parse_secret_pairs};
use diesel::connection::SimpleConnection; use diesel::connection::SimpleConnection;
use diesel::{Connection, SqliteConnection}; use diesel::{Connection, SqliteConnection};
use std::path::PathBuf; use std::path::PathBuf;
@@ -91,22 +89,6 @@ fn parse_secret_pairs_handles_valid_and_invalid_input() {
assert_eq!(err.to_string(), "Secret key cannot be empty"); assert_eq!(err.to_string(), "Secret key cannot be empty");
} }
#[test]
fn format_doppler_set_command_outputs_expected_format() {
let empty = format_doppler_set_command(&[]);
assert_eq!(empty, "doppler secrets set");
let one = format_doppler_set_command(&[("API_KEY".to_string(), "abc".to_string())]);
assert_eq!(one, "doppler secrets set \\\n API_KEY=\"abc\"");
let many = format_doppler_set_command(&[
("A".to_string(), "1".to_string()),
("B".to_string(), "2".to_string()),
]);
assert_eq!(many, "doppler secrets set \\\n A=\"1\" \\\n B=\"2\"");
}
#[test] #[test]
fn config_reads_valid_file_and_reports_errors() { fn config_reads_valid_file_and_reports_errors() {
let temp_root = std::env::temp_dir().join(format!("harbor_config_{}", Uuid::new_v4())); let temp_root = std::env::temp_dir().join(format!("harbor_config_{}", Uuid::new_v4()));
@@ -140,21 +122,27 @@ fn config_reads_valid_file_and_reports_errors() {
fn project_and_secret_lifecycle() { fn project_and_secret_lifecycle() {
let _guard = test_lock().lock().expect("lock tests"); let _guard = test_lock().lock().expect("lock tests");
let db_path = setup_test_db(); let db_path = setup_test_db();
let store = SqliteStore::new(String::new());
assert!(!project_exists("alpha").expect("project exists")); assert!(!store.project_exists("alpha").expect("project exists"));
create_project("alpha").expect("create project"); store.create_project("alpha").expect("create project");
assert!(project_exists("alpha").expect("project exists")); assert!(store.project_exists("alpha").expect("project exists"));
let project_id = get_project_id("alpha").expect("get project id"); let project_id = store.get_project_id("alpha").expect("get project id");
let projects = get_projects().expect("get projects"); let projects = store.get_projects().expect("get projects");
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(&project_id, "API_KEY", Environment::Dev).expect("secret exists")); assert!(
!store
.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( store
.set_secret(
&project_id, &project_id,
"API_KEY", "API_KEY",
b"first".to_vec(), b"first".to_vec(),
@@ -163,15 +151,22 @@ fn project_and_secret_lifecycle() {
) )
.expect("set secret"); .expect("set secret");
assert!(secret_exists(&project_id, "API_KEY", Environment::Dev).expect("secret exists")); assert!(
store
.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 = store
.get_project_secrets(&project_id, Environment::Dev)
.expect("get secrets");
assert_eq!(secrets.len(), 1); assert_eq!(secrets.len(), 1);
assert_eq!(secrets[0].0, "API_KEY"); assert_eq!(secrets[0].0, "API_KEY");
assert_eq!(secrets[0].1, b"first".to_vec()); assert_eq!(secrets[0].1, b"first".to_vec());
let nonce = crypto::helper::gen_nonce(); let nonce = crypto::helper::gen_nonce();
set_secret( store
.set_secret(
&project_id, &project_id,
"API_KEY", "API_KEY",
b"second".to_vec(), b"second".to_vec(),
@@ -180,15 +175,23 @@ fn project_and_secret_lifecycle() {
) )
.expect("update secret"); .expect("update secret");
let updated = get_project_secrets(&project_id, Environment::Dev).expect("get secrets"); let updated = store
.get_project_secrets(&project_id, Environment::Dev)
.expect("get secrets");
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(&project_id, "API_KEY", Environment::Dev).expect("delete secret"); store
assert!(!secret_exists(&project_id, "API_KEY", Environment::Dev).expect("secret exists")); .delete_secret(&project_id, "API_KEY", Environment::Dev)
.expect("delete secret");
assert!(
!store
.secret_exists(&project_id, "API_KEY", Environment::Dev)
.expect("secret exists")
);
delete_project("alpha").expect("delete project"); store.delete_project("alpha").expect("delete project");
assert!(!project_exists("alpha").expect("project exists")); assert!(!store.project_exists("alpha").expect("project exists"));
cleanup_test_db(db_path); cleanup_test_db(db_path);
} }
@@ -197,15 +200,18 @@ fn project_and_secret_lifecycle() {
fn errors_for_missing_project_or_secret() { fn errors_for_missing_project_or_secret() {
let _guard = test_lock().lock().expect("lock tests"); let _guard = test_lock().lock().expect("lock tests");
let db_path = setup_test_db(); let db_path = setup_test_db();
let store = SqliteStore::new(String::new());
let err = delete_project("missing").unwrap_err(); let err = store.delete_project("missing").unwrap_err();
assert_eq!(err.to_string(), "Project does not exist"); assert_eq!(err.to_string(), "Project does not exist");
let err = get_project_id("missing").unwrap_err(); let err = store.get_project_id("missing").unwrap_err();
assert_eq!(err.to_string(), "Project not found"); assert_eq!(err.to_string(), "Project not found");
let missing_project_id = Uuid::new_v4().to_string(); let missing_project_id = Uuid::new_v4().to_string();
let err = delete_secret(&missing_project_id, "missing", Environment::Dev).unwrap_err(); let err = store
.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);