generated from pure_sagacity/rust-starter
forgot to commit, a lot of stuff happened
This commit is contained in:
@@ -22,6 +22,8 @@ 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"
|
||||
|
||||
[[bin]]
|
||||
name = "harbor"
|
||||
|
||||
@@ -10,7 +10,8 @@ CREATE TABLE secrets (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
name 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,
|
||||
nonce BLOB NOT NULL,
|
||||
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 chrono::NaiveDateTime;
|
||||
use diesel::prelude::*;
|
||||
@@ -11,13 +12,6 @@ pub struct Project {
|
||||
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)]
|
||||
#[diesel(table_name = secrets)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
@@ -25,7 +19,7 @@ pub struct Secret {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub project_id: String,
|
||||
pub config: String,
|
||||
pub config: Environment,
|
||||
pub secret: Vec<u8>,
|
||||
pub nonce: Vec<u8>,
|
||||
pub created_at: NaiveDateTime,
|
||||
|
||||
+255
-36
@@ -1,13 +1,87 @@
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
use clap::{Parser, Subcommand};
|
||||
use colored::Colorize;
|
||||
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 serde::Deserialize;
|
||||
use std::error::Error;
|
||||
use std::io::{self, Write};
|
||||
pub mod config;
|
||||
mod db;
|
||||
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)]
|
||||
#[clap(
|
||||
name = "Harbor",
|
||||
@@ -26,24 +100,82 @@ pub struct Cli {
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
Inject {
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
after: Vec<String>,
|
||||
},
|
||||
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 {},
|
||||
List {},
|
||||
Project {
|
||||
#[command(subcommand)]
|
||||
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)]
|
||||
@@ -53,36 +185,31 @@ pub enum ProjectCommands {
|
||||
Delete { name: String },
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum ConfigCommands {
|
||||
List {},
|
||||
Create { project: String },
|
||||
Delete { project: String, name: String },
|
||||
}
|
||||
|
||||
pub mod interactions {
|
||||
use super::db::models::{NewProject, Project};
|
||||
use super::db::{
|
||||
establish_connection,
|
||||
schema::projects::dsl::{name as project_name, projects},
|
||||
};
|
||||
use super::db::establish_connection;
|
||||
use super::db::models::Project;
|
||||
use crate::Environment;
|
||||
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;
|
||||
|
||||
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(
|
||||
std::io::ErrorKind::AlreadyExists,
|
||||
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 existing_project = projects
|
||||
.filter(project_name.eq(proj_name))
|
||||
.filter(name.eq(proj_name))
|
||||
.select(Project::as_select())
|
||||
.first::<Project>(&mut conn)
|
||||
.optional()?;
|
||||
@@ -90,21 +217,22 @@ pub mod interactions {
|
||||
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();
|
||||
|
||||
if project_exists(proj_name)? {
|
||||
return construct_error("Project already exists");
|
||||
}
|
||||
|
||||
let project_id = Uuid::new_v4().to_string();
|
||||
let new_project = NewProject {
|
||||
id: &project_id,
|
||||
name: proj_name,
|
||||
};
|
||||
let proj_id = Uuid::new_v4().to_string();
|
||||
|
||||
match diesel::insert_into(projects)
|
||||
.values(&new_project)
|
||||
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(()),
|
||||
@@ -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();
|
||||
|
||||
if !project_exists(proj_name)? {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gen_or_get_key() -> Result<crypto::Key, Box<dyn Error>> {
|
||||
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();
|
||||
|
||||
if new_line {
|
||||
|
||||
+249
-25
@@ -3,17 +3,27 @@ use std::process;
|
||||
use clap::Parser;
|
||||
use clap::crate_version;
|
||||
use cli::Cli;
|
||||
use cli::Environment;
|
||||
use cli::config::{Config, ConfigError};
|
||||
use cli::gen_or_get_key;
|
||||
use cli::get_input;
|
||||
use cli::interactions::delete_secret;
|
||||
use cli::interactions::get_project_id;
|
||||
use cli::interactions::get_projects;
|
||||
use cli::interactions::set_secret;
|
||||
use colored::*;
|
||||
use crypto::encrypt;
|
||||
|
||||
const GIT_VERSION: &str = env!("GIT_VERSION");
|
||||
|
||||
fn main() {
|
||||
use cli::{Commands, ConfigCommands, ProjectCommands};
|
||||
use cli::{Commands, ProjectCommands};
|
||||
let cli = Cli::parse();
|
||||
|
||||
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 =
|
||||
format!("An open source secrets management and distribution platform.").blue();
|
||||
let commit_msg = format!("Git commit {}", GIT_VERSION).dimmed();
|
||||
@@ -22,39 +32,146 @@ fn main() {
|
||||
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 {
|
||||
Some(c) => match c {
|
||||
Commands::Add {} => {
|
||||
println!("Add command executed");
|
||||
}
|
||||
Commands::Config { command } => match command {
|
||||
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
|
||||
);
|
||||
}
|
||||
},
|
||||
Commands::Inject { verbose, after } => {
|
||||
println!(
|
||||
"Inject command executed with verbose: {}, after: {:?}",
|
||||
verbose, after
|
||||
_ if requires_config(&c) && !has_config => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
"Missing .harbor.toml. Run `harbor config create` first.".yellow()
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
Commands::Set { environment, vars } => {
|
||||
let config = require_config(&root);
|
||||
let environment: Environment = match environment {
|
||||
Some(env) => match env.parse::<Environment>() {
|
||||
Ok(parsed) => parsed,
|
||||
Err(_) => {
|
||||
eprintln!("Invalid environment '{}'.", env);
|
||||
process::exit(1);
|
||||
}
|
||||
},
|
||||
None => config.default_env.into(),
|
||||
};
|
||||
|
||||
let pairs = match cli::parse_secret_pairs(&vars) {
|
||||
Ok(pairs) => pairs,
|
||||
Err(e) => {
|
||||
eprintln!("Error parsing secrets: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if pairs.is_empty() {
|
||||
eprintln!("No secrets provided. Use KEY=VALUE pairs.");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let project = match get_project_id(&config.name) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Unable to get project ID for '{}': {}", config.name, e);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
for pair in pairs {
|
||||
let key = match gen_or_get_key() {
|
||||
Ok(k) => k,
|
||||
Err(_) => {
|
||||
eprintln!("Error generating or getting key");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
let (nonce, encrypted) = match encrypt(&key, pair.1.as_bytes().to_vec()) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
eprintln!("Error encrypting secret for key '{}': {}", pair.0, e);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
match set_secret(&project, &pair.0, encrypted, environment, nonce) {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
eprintln!("Error setting secret for key '{}': {}", pair.0, e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Commands::Delete { keys } => {
|
||||
let mut cleaned = Vec::new();
|
||||
|
||||
for key in keys {
|
||||
let trimmed = key.trim();
|
||||
if !trimmed.is_empty() {
|
||||
cleaned.push(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Redefining as immutable
|
||||
let cleaned = cleaned;
|
||||
|
||||
if cleaned.is_empty() {
|
||||
eprintln!("No secret keys provided. Use one or more keys.");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
for key in cleaned {
|
||||
match delete_secret(&key) {
|
||||
Ok(()) => {
|
||||
println!("Deleted secret for key '{}'", key);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error deleting secret for key '{}': {}", key, e);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Commands::Inject { after } => {
|
||||
let _config = require_config(&root);
|
||||
|
||||
// for harbor inject -- bun dev
|
||||
// after = ["bun", "dev"]
|
||||
|
||||
|
||||
}
|
||||
Commands::List {} => {
|
||||
let _config = require_config(&root);
|
||||
|
||||
println!("List command executed");
|
||||
}
|
||||
Commands::Project { command } => match command {
|
||||
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 {} => {
|
||||
let project_name = match get_input("Project name", ':', false) {
|
||||
@@ -124,7 +241,86 @@ fn main() {
|
||||
println!("Shell command executed");
|
||||
}
|
||||
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 => {
|
||||
@@ -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