generated from pure_sagacity/rust-starter
switched to using the Store trait
also added another dependency for expanding ~ paths
This commit is contained in:
Generated
+48
@@ -448,6 +448,7 @@ dependencies = [
|
||||
"colored",
|
||||
"crypto",
|
||||
"diesel",
|
||||
"dirs",
|
||||
"dotenvy",
|
||||
"hex",
|
||||
"keyring",
|
||||
@@ -653,6 +654,27 @@ dependencies = [
|
||||
"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]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.7"
|
||||
@@ -1024,6 +1046,15 @@ version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.37.0"
|
||||
@@ -1171,6 +1202,12 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
@@ -1322,6 +1359,17 @@ dependencies = [
|
||||
"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]]
|
||||
name = "regex"
|
||||
version = "1.13.0"
|
||||
|
||||
@@ -13,17 +13,18 @@ diesel = { version = "2.2.0", features = [
|
||||
"chrono",
|
||||
] }
|
||||
clap = { version = "4.6.1", features = ["derive", "cargo"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
tokio = { version = "1.52.3", features = ["full"] }
|
||||
uuid = { version = "1.23.5", features = ["v4"] }
|
||||
crypto = { path = "../crypto" }
|
||||
keyring = "4.1.4"
|
||||
colored = "3.1.1"
|
||||
base64 = "0.22.1"
|
||||
chrono = "0.4.45"
|
||||
dotenvy = "0.15"
|
||||
uuid = { version = "1.23.5", features = ["v4"] }
|
||||
hex = "0.4"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
toml = "1.1.2"
|
||||
hex = "0.4"
|
||||
dirs = "6"
|
||||
|
||||
[[bin]]
|
||||
name = "harbor"
|
||||
|
||||
+8
-232
@@ -10,9 +10,10 @@ use keyring::Entry;
|
||||
use serde::Deserialize;
|
||||
use std::error::Error;
|
||||
use std::io::{self, Write};
|
||||
use std::path::PathBuf;
|
||||
pub mod config;
|
||||
mod db;
|
||||
mod store;
|
||||
pub mod store;
|
||||
|
||||
#[derive(
|
||||
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)
|
||||
}
|
||||
|
||||
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)]
|
||||
pub enum ProjectCommands {
|
||||
List {},
|
||||
@@ -206,217 +183,16 @@ pub enum ProjectCommands {
|
||||
Delete { name: String },
|
||||
}
|
||||
|
||||
const DB_URL: &str = "/Users/Maaz/Documents/Git/harbor/crates/cli/harbor.db";
|
||||
|
||||
pub mod interactions {
|
||||
use super::DB_URL;
|
||||
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 expand_tilde(path: &str) -> String {
|
||||
if let Some(stripped) = path.strip_prefix("~/") {
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
return home.join(stripped).to_string_lossy().to_string();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_project(proj_name: &str) -> Result<()> {
|
||||
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(())
|
||||
}
|
||||
path.to_string()
|
||||
}
|
||||
|
||||
pub fn gen_or_get_key() -> Result<crypto::Key, Box<dyn Error>> {
|
||||
let entry = Entry::new("harbor", "encryption-key")?;
|
||||
|
||||
|
||||
+25
-33
@@ -6,14 +6,11 @@ use clap::crate_version;
|
||||
use cli::Cli;
|
||||
use cli::Environment;
|
||||
use cli::config::{Config, ConfigError};
|
||||
use cli::expand_tilde;
|
||||
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::secret_exists;
|
||||
use cli::interactions::set_secret;
|
||||
use cli::store::Store;
|
||||
use cli::store::sqlite::SqliteStore;
|
||||
use colored::*;
|
||||
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 has_config = config_path.exists();
|
||||
let store = SqliteStore::new(db_path);
|
||||
|
||||
match cli.command {
|
||||
Some(c) => match c {
|
||||
@@ -69,7 +68,7 @@ fn main() {
|
||||
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,
|
||||
Err(e) => {
|
||||
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,
|
||||
Err(e) => {
|
||||
print_error(format!("Error getting secrets: {}", e));
|
||||
@@ -156,7 +155,7 @@ fn main() {
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let project = match get_project_id(&config.name) {
|
||||
let project = match store.get_project_id(&config.name) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
print_error(format!(
|
||||
@@ -168,7 +167,7 @@ fn main() {
|
||||
};
|
||||
|
||||
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,
|
||||
Err(e) => {
|
||||
print_error(format!(
|
||||
@@ -180,10 +179,7 @@ fn main() {
|
||||
};
|
||||
if existed {
|
||||
let confirmation = match get_input(
|
||||
format!(
|
||||
"Secret '{}' already exists. Overwrite? (y/N)",
|
||||
pair.0
|
||||
)
|
||||
format!("Secret '{}' already exists. Overwrite? (y/N)", pair.0)
|
||||
.yellow(),
|
||||
':',
|
||||
false,
|
||||
@@ -198,11 +194,7 @@ fn main() {
|
||||
if confirmation.to_lowercase() != "y" {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"Skipped secret '{}' in {}.",
|
||||
pair.0,
|
||||
environment.as_str()
|
||||
)
|
||||
format!("Skipped secret '{}' in {}.", pair.0, environment.as_str())
|
||||
.dimmed()
|
||||
);
|
||||
continue;
|
||||
@@ -226,7 +218,7 @@ fn main() {
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
match set_secret(&project, &pair.0, encrypted, environment, nonce) {
|
||||
match store.set_secret(&project, &pair.0, encrypted, environment, nonce) {
|
||||
Ok(()) => {
|
||||
let verb = if existed { "Updated" } else { "Set" };
|
||||
println!(
|
||||
@@ -263,7 +255,7 @@ fn main() {
|
||||
},
|
||||
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,
|
||||
Err(e) => {
|
||||
print_error(format!(
|
||||
@@ -291,7 +283,7 @@ fn main() {
|
||||
}
|
||||
|
||||
for key in cleaned {
|
||||
match delete_secret(&proj_id, &key, environment) {
|
||||
match store.delete_secret(&proj_id, &key, environment) {
|
||||
Ok(()) => {
|
||||
println!("Deleted secret for key '{}'", key);
|
||||
}
|
||||
@@ -314,7 +306,7 @@ fn main() {
|
||||
},
|
||||
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,
|
||||
Err(e) => {
|
||||
print_error(format!(
|
||||
@@ -334,7 +326,7 @@ fn main() {
|
||||
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,
|
||||
Err(e) => {
|
||||
print_error(format!("Error getting secrets: {}", e));
|
||||
@@ -392,7 +384,7 @@ fn main() {
|
||||
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) {
|
||||
let proj_id = match store.get_project_id(&config.name) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
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,
|
||||
Err(e) => {
|
||||
print_error(format!("Error getting secrets: {}", e));
|
||||
@@ -429,7 +421,7 @@ fn main() {
|
||||
}
|
||||
Commands::Project { command } => match command {
|
||||
ProjectCommands::List {} => {
|
||||
let projects = match get_projects() {
|
||||
let projects = match store.get_projects() {
|
||||
Ok(projects) => projects,
|
||||
Err(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(()) => {
|
||||
println!("{}", "Project created successfully.".bright_green().bold())
|
||||
}
|
||||
@@ -461,7 +453,7 @@ fn main() {
|
||||
}
|
||||
ProjectCommands::Delete { name } => {
|
||||
// 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,
|
||||
Err(e) => {
|
||||
print_error(format!("Error checking if project exists: {}", e));
|
||||
@@ -492,7 +484,7 @@ fn main() {
|
||||
};
|
||||
|
||||
if confirmation.to_lowercase() == "y" {
|
||||
match cli::interactions::delete_project(&name) {
|
||||
match store.delete_project(&name) {
|
||||
Ok(()) => {
|
||||
println!(
|
||||
"{}",
|
||||
@@ -525,7 +517,7 @@ fn main() {
|
||||
},
|
||||
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,
|
||||
Err(e) => {
|
||||
print_error(format!(
|
||||
@@ -551,7 +543,7 @@ fn main() {
|
||||
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,
|
||||
Err(e) => {
|
||||
print_error(format!("Error getting secrets: {}", e));
|
||||
@@ -618,7 +610,7 @@ fn main() {
|
||||
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() {
|
||||
let projects = match store.get_projects() {
|
||||
Ok(projects) => projects,
|
||||
Err(e) => {
|
||||
print_error(format!("Error getting projects: {}", e));
|
||||
|
||||
@@ -3,5 +3,35 @@
|
||||
// With this trait, we can implement other savers like Postgres, MySQL, etc.
|
||||
// Or even third party services like Hashicorp Vault, AWS Secrets Manager, etc.
|
||||
|
||||
// For now, I'll just get an MVP in place.
|
||||
pub trait Store {}
|
||||
use std::error::Error;
|
||||
|
||||
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<()>;
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
use cli::Environment;
|
||||
use cli::config::Config;
|
||||
use cli::interactions::{
|
||||
create_project, delete_project, 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::parse_secret_pairs;
|
||||
use cli::store::Store;
|
||||
use cli::store::sqlite::SqliteStore;
|
||||
use diesel::connection::SimpleConnection;
|
||||
use diesel::{Connection, SqliteConnection};
|
||||
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");
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn config_reads_valid_file_and_reports_errors() {
|
||||
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() {
|
||||
let _guard = test_lock().lock().expect("lock tests");
|
||||
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");
|
||||
assert!(project_exists("alpha").expect("project exists"));
|
||||
store.create_project("alpha").expect("create project");
|
||||
assert!(store.project_exists("alpha").expect("project exists"));
|
||||
|
||||
let project_id = get_project_id("alpha").expect("get project id");
|
||||
let projects = get_projects().expect("get projects");
|
||||
let project_id = store.get_project_id("alpha").expect("get project id");
|
||||
let projects = store.get_projects().expect("get projects");
|
||||
assert_eq!(projects.len(), 1);
|
||||
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();
|
||||
set_secret(
|
||||
store
|
||||
.set_secret(
|
||||
&project_id,
|
||||
"API_KEY",
|
||||
b"first".to_vec(),
|
||||
@@ -163,15 +151,22 @@ fn project_and_secret_lifecycle() {
|
||||
)
|
||||
.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[0].0, "API_KEY");
|
||||
assert_eq!(secrets[0].1, b"first".to_vec());
|
||||
|
||||
let nonce = crypto::helper::gen_nonce();
|
||||
set_secret(
|
||||
store
|
||||
.set_secret(
|
||||
&project_id,
|
||||
"API_KEY",
|
||||
b"second".to_vec(),
|
||||
@@ -180,15 +175,23 @@ fn project_and_secret_lifecycle() {
|
||||
)
|
||||
.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[0].1, b"second".to_vec());
|
||||
|
||||
delete_secret(&project_id, "API_KEY", Environment::Dev).expect("delete secret");
|
||||
assert!(!secret_exists(&project_id, "API_KEY", Environment::Dev).expect("secret exists"));
|
||||
store
|
||||
.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");
|
||||
assert!(!project_exists("alpha").expect("project exists"));
|
||||
store.delete_project("alpha").expect("delete project");
|
||||
assert!(!store.project_exists("alpha").expect("project exists"));
|
||||
|
||||
cleanup_test_db(db_path);
|
||||
}
|
||||
@@ -197,15 +200,18 @@ fn project_and_secret_lifecycle() {
|
||||
fn errors_for_missing_project_or_secret() {
|
||||
let _guard = test_lock().lock().expect("lock tests");
|
||||
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");
|
||||
|
||||
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");
|
||||
|
||||
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");
|
||||
|
||||
cleanup_test_db(db_path);
|
||||
|
||||
Reference in New Issue
Block a user