generated from pure_sagacity/rust-starter
forgot to commit, a lot of stuff happened
This commit is contained in:
Generated
+32
@@ -451,7 +451,9 @@ dependencies = [
|
|||||||
"dotenvy",
|
"dotenvy",
|
||||||
"hex",
|
"hex",
|
||||||
"keyring",
|
"keyring",
|
||||||
|
"serde",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1467,6 +1469,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
@@ -1642,6 +1653,21 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "1.1.2+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde_core",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_parser",
|
||||||
|
"toml_writer",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "1.1.1+spec-1.1.0"
|
version = "1.1.1+spec-1.1.0"
|
||||||
@@ -1672,6 +1698,12 @@ dependencies = [
|
|||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_writer"
|
||||||
|
version = "1.1.1+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.44"
|
version = "0.1.44"
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ chrono = "0.4.45"
|
|||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
uuid = { version = "1.23.5", features = ["v4"] }
|
uuid = { version = "1.23.5", features = ["v4"] }
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
toml = "1.1.2"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "harbor"
|
name = "harbor"
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ CREATE TABLE secrets (
|
|||||||
id TEXT PRIMARY KEY NOT NULL,
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
project_id TEXT NOT NULL,
|
project_id TEXT NOT NULL,
|
||||||
config TEXT NOT NULL,
|
config TEXT NOT NULL
|
||||||
|
CHECK (config IN ('dev', 'prod', 'staging')),
|
||||||
secret BLOB NOT NULL,
|
secret BLOB NOT NULL,
|
||||||
nonce BLOB NOT NULL,
|
nonce BLOB NOT NULL,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
use crate::Environment;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
const MAIN_CONFIG_FILE: &str = ".harbor.toml";
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Config {
|
||||||
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
pub default_env: Environment,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
Io(std::io::Error),
|
||||||
|
Toml(toml::de::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ConfigError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ConfigError::Io(err) => write!(f, "IO error: {}", err),
|
||||||
|
ConfigError::Toml(err) => write!(f, "TOML error: {}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ConfigError {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
ConfigError::Io(err) => Some(err),
|
||||||
|
ConfigError::Toml(err) => Some(err),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for ConfigError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
ConfigError::Io(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<toml::de::Error> for ConfigError {
|
||||||
|
fn from(err: toml::de::Error) -> Self {
|
||||||
|
ConfigError::Toml(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||||
|
pub struct HarborToml {
|
||||||
|
#[serde(alias = "project")]
|
||||||
|
name: String,
|
||||||
|
version: String,
|
||||||
|
config: Environment,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn from_repo_root(root: impl AsRef<Path>) -> Result<Self, ConfigError> {
|
||||||
|
let root = root.as_ref();
|
||||||
|
let main = Self::read_main_file(root.join(MAIN_CONFIG_FILE))?;
|
||||||
|
|
||||||
|
Ok(Config {
|
||||||
|
name: main.name,
|
||||||
|
version: main.version,
|
||||||
|
default_env: main.config,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_main_file(path: impl AsRef<Path>) -> Result<HarborToml, ConfigError> {
|
||||||
|
let contents = fs::read_to_string(path)?;
|
||||||
|
let parsed: HarborToml = toml::from_str(&contents)?;
|
||||||
|
Ok(parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merged_env_vars(&self, store_vars: &HashMap<String, String>) -> HashMap<String, String> {
|
||||||
|
store_vars.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::Environment;
|
||||||
use crate::db::schema::{projects, secrets};
|
use crate::db::schema::{projects, secrets};
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
@@ -11,13 +12,6 @@ pub struct Project {
|
|||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable)]
|
|
||||||
#[diesel(table_name = projects)]
|
|
||||||
pub struct NewProject<'a> {
|
|
||||||
pub id: &'a str,
|
|
||||||
pub name: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Queryable, Selectable)]
|
#[derive(Debug, Queryable, Selectable)]
|
||||||
#[diesel(table_name = secrets)]
|
#[diesel(table_name = secrets)]
|
||||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
@@ -25,7 +19,7 @@ pub struct Secret {
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub project_id: String,
|
pub project_id: String,
|
||||||
pub config: String,
|
pub config: Environment,
|
||||||
pub secret: Vec<u8>,
|
pub secret: Vec<u8>,
|
||||||
pub nonce: Vec<u8>,
|
pub nonce: Vec<u8>,
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
|
|||||||
+255
-36
@@ -1,13 +1,87 @@
|
|||||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use colored::Colorize;
|
|
||||||
use crypto::helper::gen_key;
|
use crypto::helper::gen_key;
|
||||||
|
use diesel::backend::Backend;
|
||||||
|
use diesel::deserialize::{self, FromSql};
|
||||||
|
use diesel::serialize::{self, IsNull, Output, ToSql};
|
||||||
|
use diesel::sql_types::Text;
|
||||||
|
use diesel::sqlite::Sqlite;
|
||||||
use keyring::Entry;
|
use keyring::Entry;
|
||||||
|
use serde::Deserialize;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
pub mod config;
|
||||||
mod db;
|
mod db;
|
||||||
mod store;
|
mod store;
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, Copy, PartialEq, Eq, Deserialize, diesel::AsExpression, diesel::FromSqlRow,
|
||||||
|
)]
|
||||||
|
#[diesel(sql_type = Text)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum Environment {
|
||||||
|
Dev,
|
||||||
|
Prod,
|
||||||
|
Staging,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct EnvironmentParseError(String);
|
||||||
|
|
||||||
|
impl Environment {
|
||||||
|
pub const fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Environment::Dev => "dev",
|
||||||
|
Environment::Prod => "prod",
|
||||||
|
Environment::Staging => "staging",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Environment {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(self.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for Environment {
|
||||||
|
type Err = EnvironmentParseError;
|
||||||
|
|
||||||
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||||
|
match value.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"dev" => Ok(Environment::Dev),
|
||||||
|
"prod" => Ok(Environment::Prod),
|
||||||
|
"staging" => Ok(Environment::Staging),
|
||||||
|
_ => Err(EnvironmentParseError(
|
||||||
|
"Environment must be one of: dev, prod, staging".to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for EnvironmentParseError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for EnvironmentParseError {}
|
||||||
|
|
||||||
|
impl ToSql<Text, Sqlite> for Environment {
|
||||||
|
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
|
||||||
|
out.set_value(self.as_str());
|
||||||
|
Ok(IsNull::No)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromSql<Text, Sqlite> for Environment {
|
||||||
|
fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
|
||||||
|
let raw = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
|
||||||
|
raw.parse::<Environment>()
|
||||||
|
.map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[clap(
|
#[clap(
|
||||||
name = "Harbor",
|
name = "Harbor",
|
||||||
@@ -26,24 +100,82 @@ pub struct Cli {
|
|||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
Inject {
|
Inject {
|
||||||
#[arg(short, long)]
|
|
||||||
verbose: bool,
|
|
||||||
|
|
||||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||||
after: Vec<String>,
|
after: Vec<String>,
|
||||||
},
|
},
|
||||||
Shell {},
|
Shell {},
|
||||||
Add {},
|
#[command(alias = "add")]
|
||||||
|
Set {
|
||||||
|
#[arg(short = 'e', long = "environment")]
|
||||||
|
environment: Option<String>,
|
||||||
|
|
||||||
|
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||||
|
vars: Vec<String>,
|
||||||
|
},
|
||||||
|
Delete {
|
||||||
|
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||||
|
keys: Vec<String>,
|
||||||
|
},
|
||||||
Setup {},
|
Setup {},
|
||||||
List {},
|
List {},
|
||||||
Project {
|
Project {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: ProjectCommands,
|
command: ProjectCommands,
|
||||||
},
|
},
|
||||||
Config {
|
}
|
||||||
#[command(subcommand)]
|
|
||||||
command: ConfigCommands,
|
pub fn parse_secret_pairs(raw: &[String]) -> Result<Vec<(String, String)>, Box<dyn Error>> {
|
||||||
},
|
let mut pairs = Vec::new();
|
||||||
|
|
||||||
|
for item in raw {
|
||||||
|
let trimmed = item.trim();
|
||||||
|
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (key, value) = trimmed.split_once('=').ok_or_else(|| {
|
||||||
|
Box::new(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidInput,
|
||||||
|
format!("Invalid secret format: {}", item),
|
||||||
|
)) as Box<dyn Error>
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if key.trim().is_empty() {
|
||||||
|
return Err(Box::new(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidInput,
|
||||||
|
"Secret key cannot be empty",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
pairs.push((key.to_string(), value.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
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)]
|
||||||
@@ -53,36 +185,31 @@ pub enum ProjectCommands {
|
|||||||
Delete { name: String },
|
Delete { name: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
pub enum ConfigCommands {
|
|
||||||
List {},
|
|
||||||
Create { project: String },
|
|
||||||
Delete { project: String, name: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod interactions {
|
pub mod interactions {
|
||||||
use super::db::models::{NewProject, Project};
|
use super::db::establish_connection;
|
||||||
use super::db::{
|
use super::db::models::Project;
|
||||||
establish_connection,
|
use crate::Environment;
|
||||||
schema::projects::dsl::{name as project_name, projects},
|
use diesel::dsl::{insert_into, update};
|
||||||
};
|
|
||||||
use diesel::result::{DatabaseErrorKind, Error as DieselError};
|
use diesel::result::{DatabaseErrorKind, Error as DieselError};
|
||||||
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper};
|
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
fn construct_error(message: &str) -> Result<(), Box<dyn Error>> {
|
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||||
|
|
||||||
|
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,
|
||||||
message.to_string(),
|
message.to_string(),
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn project_exists(proj_name: &str) -> Result<bool, Box<dyn Error>> {
|
pub fn project_exists(proj_name: &str) -> Result<bool> {
|
||||||
|
use super::db::schema::projects::dsl::{name, projects};
|
||||||
let mut conn = establish_connection();
|
let mut conn = establish_connection();
|
||||||
|
|
||||||
let existing_project = projects
|
let existing_project = projects
|
||||||
.filter(project_name.eq(proj_name))
|
.filter(name.eq(proj_name))
|
||||||
.select(Project::as_select())
|
.select(Project::as_select())
|
||||||
.first::<Project>(&mut conn)
|
.first::<Project>(&mut conn)
|
||||||
.optional()?;
|
.optional()?;
|
||||||
@@ -90,21 +217,22 @@ pub mod interactions {
|
|||||||
Ok(existing_project.is_some())
|
Ok(existing_project.is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_project(proj_name: &str) -> Result<(), Box<dyn Error>> {
|
pub fn create_project(proj_name: &str) -> Result<()> {
|
||||||
|
use super::db::schema::projects::dsl::{created_at, id, name, projects};
|
||||||
let mut conn = establish_connection();
|
let mut conn = establish_connection();
|
||||||
|
|
||||||
if project_exists(proj_name)? {
|
if project_exists(proj_name)? {
|
||||||
return construct_error("Project already exists");
|
return construct_error("Project already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
let project_id = Uuid::new_v4().to_string();
|
let proj_id = Uuid::new_v4().to_string();
|
||||||
let new_project = NewProject {
|
|
||||||
id: &project_id,
|
|
||||||
name: proj_name,
|
|
||||||
};
|
|
||||||
|
|
||||||
match diesel::insert_into(projects)
|
match insert_into(projects)
|
||||||
.values(&new_project)
|
.values((
|
||||||
|
id.eq(proj_id),
|
||||||
|
name.eq(proj_name),
|
||||||
|
created_at.eq(chrono::Utc::now().naive_utc()),
|
||||||
|
))
|
||||||
.execute(&mut conn)
|
.execute(&mut conn)
|
||||||
{
|
{
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
@@ -115,19 +243,106 @@ pub mod interactions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_project(proj_name: &str) -> Result<(), Box<dyn Error>> {
|
pub fn delete_project(proj_name: &str) -> Result<()> {
|
||||||
|
use super::db::schema::projects::dsl::{name, projects};
|
||||||
let mut conn = establish_connection();
|
let mut conn = establish_connection();
|
||||||
|
|
||||||
if !project_exists(proj_name)? {
|
if !project_exists(proj_name)? {
|
||||||
return construct_error("Project does not exist");
|
return construct_error("Project does not exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::delete(projects.filter(project_name.eq(proj_name))).execute(&mut conn)?;
|
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();
|
||||||
|
|
||||||
|
let results = projects
|
||||||
|
.select(Project::as_select())
|
||||||
|
.load::<Project>(&mut conn)?;
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn secret_exists() -> Result<bool> {
|
||||||
|
use super::db::schema::secrets::dsl::{id, secrets};
|
||||||
|
let mut conn = establish_connection();
|
||||||
|
|
||||||
|
let existing_secret = secrets.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();
|
||||||
|
|
||||||
|
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, name, nonce, project_id, secret, secrets,
|
||||||
|
};
|
||||||
|
let mut conn = establish_connection();
|
||||||
|
|
||||||
|
if secret_exists()? {
|
||||||
|
update(secrets.filter(name.eq(secret_name)))
|
||||||
|
.set(secret.eq(secret_value))
|
||||||
|
.execute(&mut conn)?;
|
||||||
|
} else {
|
||||||
|
insert_into(secrets)
|
||||||
|
.values((
|
||||||
|
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 delete_secret(secret_name: &str) -> Result<()> {
|
||||||
|
use super::db::schema::secrets::dsl::{name, secrets};
|
||||||
|
let mut conn = establish_connection();
|
||||||
|
|
||||||
|
if !secret_exists()? {
|
||||||
|
return Err(Box::new(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::NotFound,
|
||||||
|
"Secret does not exist",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::delete(secrets.filter(name.eq(secret_name))).execute(&mut conn)?;
|
||||||
|
|
||||||
Ok(())
|
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")?;
|
||||||
|
|
||||||
@@ -153,7 +368,11 @@ pub fn gen_or_get_key() -> Result<crypto::Key, Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_input(message: impl std::fmt::Display, prompt: char, new_line: bool) -> Result<String, Box<dyn Error>> {
|
pub fn get_input(
|
||||||
|
message: impl std::fmt::Display,
|
||||||
|
prompt: char,
|
||||||
|
new_line: bool,
|
||||||
|
) -> Result<String, Box<dyn Error>> {
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
|
|
||||||
if new_line {
|
if new_line {
|
||||||
|
|||||||
+247
-23
@@ -3,17 +3,27 @@ use std::process;
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use clap::crate_version;
|
use clap::crate_version;
|
||||||
use cli::Cli;
|
use cli::Cli;
|
||||||
|
use cli::Environment;
|
||||||
|
use cli::config::{Config, ConfigError};
|
||||||
|
use cli::gen_or_get_key;
|
||||||
use cli::get_input;
|
use cli::get_input;
|
||||||
|
use cli::interactions::delete_secret;
|
||||||
|
use cli::interactions::get_project_id;
|
||||||
|
use cli::interactions::get_projects;
|
||||||
|
use cli::interactions::set_secret;
|
||||||
use colored::*;
|
use colored::*;
|
||||||
|
use crypto::encrypt;
|
||||||
|
|
||||||
const GIT_VERSION: &str = env!("GIT_VERSION");
|
const GIT_VERSION: &str = env!("GIT_VERSION");
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
use cli::{Commands, ConfigCommands, ProjectCommands};
|
use cli::{Commands, ProjectCommands};
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
if cli.version {
|
if cli.version {
|
||||||
let top_msg = format!("Harbor version {}", crate_version!().green()).bright_cyan();
|
let top_msg = format!("Harbor version {}", crate_version!().green())
|
||||||
|
.bright_cyan()
|
||||||
|
.bold();
|
||||||
let bottom_msg =
|
let bottom_msg =
|
||||||
format!("An open source secrets management and distribution platform.").blue();
|
format!("An open source secrets management and distribution platform.").blue();
|
||||||
let commit_msg = format!("Git commit {}", GIT_VERSION).dimmed();
|
let commit_msg = format!("Git commit {}", GIT_VERSION).dimmed();
|
||||||
@@ -22,39 +32,146 @@ fn main() {
|
|||||||
process::exit(0);
|
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();
|
||||||
|
|
||||||
|
if !has_config {
|
||||||
|
eprintln!(
|
||||||
|
"{}",
|
||||||
|
"Warning: .harbor.toml not found. Some commands are disabled.".yellow()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Some(c) => match c {
|
Some(c) => match c {
|
||||||
Commands::Add {} => {
|
_ if requires_config(&c) && !has_config => {
|
||||||
println!("Add command executed");
|
eprintln!(
|
||||||
}
|
"{}",
|
||||||
Commands::Config { command } => match command {
|
"Missing .harbor.toml. Run `harbor config create` first.".yellow()
|
||||||
ConfigCommands::List {} => {
|
|
||||||
println!("Config list command executed");
|
|
||||||
}
|
|
||||||
ConfigCommands::Create { project } => {
|
|
||||||
println!("Config create command executed for project: {}", project);
|
|
||||||
}
|
|
||||||
ConfigCommands::Delete { project, name } => {
|
|
||||||
println!(
|
|
||||||
"Config delete command executed for project: {}, name: {}",
|
|
||||||
project, name
|
|
||||||
);
|
);
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Commands::Inject { verbose, after } => {
|
None => config.default_env.into(),
|
||||||
println!(
|
};
|
||||||
"Inject command executed with verbose: {}, after: {:?}",
|
|
||||||
verbose, after
|
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 { after } => {
|
||||||
|
let _config = require_config(&root);
|
||||||
|
|
||||||
// for harbor inject -- bun dev
|
// for harbor inject -- bun dev
|
||||||
// after = ["bun", "dev"]
|
// after = ["bun", "dev"]
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
Commands::List {} => {
|
Commands::List {} => {
|
||||||
|
let _config = require_config(&root);
|
||||||
|
|
||||||
println!("List command executed");
|
println!("List command executed");
|
||||||
}
|
}
|
||||||
Commands::Project { command } => match command {
|
Commands::Project { command } => match command {
|
||||||
ProjectCommands::List {} => {
|
ProjectCommands::List {} => {
|
||||||
println!("Project list command executed");
|
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 {} => {
|
ProjectCommands::Create {} => {
|
||||||
let project_name = match get_input("Project name", ':', false) {
|
let project_name = match get_input("Project name", ':', false) {
|
||||||
@@ -124,7 +241,86 @@ fn main() {
|
|||||||
println!("Shell command executed");
|
println!("Shell command executed");
|
||||||
}
|
}
|
||||||
Commands::Setup {} => {
|
Commands::Setup {} => {
|
||||||
println!("Setup command executed");
|
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 => {
|
None => {
|
||||||
@@ -137,3 +333,31 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 { .. }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user