diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 767ddc7..78ec265 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - rust: [stable, beta, nightly, 1.64.0] + rust: [stable, beta, nightly, 1.73.0] steps: - uses: actions/checkout@v3 @@ -30,6 +30,10 @@ jobs: command: test args: --no-run + - name: Build CLI + run: | + cargo build --bin dotenv-vault --features=cli + - name: Run tests uses: actions-rs/cargo@v1 with: diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..457c5fe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "rust-analyzer.cargo.features": "all", + "rust-analyzer.cargo.allTargets": true, +} diff --git a/Cargo.toml b/Cargo.toml index 2604154..aef99ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,15 +9,27 @@ keywords = ["dotenv", "env", "environment", "settings", "vault"] categories = ["config"] license = "MIT" edition = "2021" -rust-version = "1.64.0" +rust-version = "1.73.0" + +[[bin]] +name = "dotenv-vault" +required-features = ["cli"] + +[lib] + +[features] +default = [] +cli = ["dep:argh"] [dependencies] aes-gcm = "0.10.2" -base64 = "0.21.2" +argh = { version = "0.1.12", optional = true } +base64 = "0.22.1" dotenvy = "0.15.7" hex = "0.4.3" url = "2.4.0" [dev-dependencies] -serial_test = "2.0.0" +serial_test = "3.1.1" tempfile = "3.7.0" +assert_cmd = { version = "2.0.14", features = ["color-auto"] } diff --git a/README.md b/README.md index 96d42f4..85a8445 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![crates.io](https://img.shields.io/crates/v/dotenv-vault.svg)](https://crates.io/crates/dotenv-vault) [![msrv -1.64.0](https://img.shields.io/badge/msrv-1.64.0-dea584.svg?logo=rust)](https://github.com/rust-lang/rust/releases/tag/1.64.0) +1.73.0](https://img.shields.io/badge/msrv-1.73.0-dea584.svg?logo=rust)](https://github.com/rust-lang/rust/releases/tag/1.73.0) [![ci](https://github.com/Minebomber/dotenv-vault-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/Minebomber/dotenv-vault-rs/actions/workflows/ci.yml) [![docs](https://img.shields.io/docsrs/dotenv-vault?logo=docs.rs)](https://docs.rs/dotenv-vault/) @@ -17,6 +17,26 @@ The extended standard lets you load encrypted secrets from your `.env.vault` fil * [FAQ](#faq) * [Changelog](./CHANGELOG.md) +## Install CLI + +The dotenv-vault CLI allows loading the `.env.vault` file and run the given program with the environment variables set. + +```shell +cargo install dotenv-vault --features cli +``` + +## Usage CLI + +```shell +dotenv-vault run -- some_program arg1 arg2 +``` + +or run at a different working directory that contains the `.env.vault` and override existing environment variables: + +```shell +dotenv-vault run --cwd ./some_folder --override -- some_program arg1 arg2 +``` + ## Install ```shell diff --git a/src/lib.rs b/src/lib.rs index 1ba1460..273e7f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,7 +74,7 @@ mod tests { let tmp = tempdir().unwrap(); let vault_path = tmp.path().join(".env.vault"); - let mut vault = File::create(&vault_path).unwrap(); + let mut vault = File::create(vault_path).unwrap(); vault .write_all("DOTENV_VAULT_PRODUCTION=\"s7NYXa809k/bVSPwIAmJhPJmEGTtU0hG58hOZy7I0ix6y5HP8LsHBsZCYC/gw5DDFy5DgOcyd18R\"".as_bytes()) .unwrap(); @@ -101,7 +101,7 @@ mod tests { fn dotenv_fallback_to_env() { let tmp = tempdir().unwrap(); let env_path = tmp.path().join(".env"); - let mut env = File::create(&env_path).unwrap(); + let mut env = File::create(env_path).unwrap(); env.write_all("TESTKEY=\"from .env\"".as_bytes()).unwrap(); env.sync_all().unwrap(); @@ -127,7 +127,7 @@ mod tests { let tmp = tempdir().unwrap(); let vault_path = tmp.path().join(".env.vault"); - let mut vault = File::create(&vault_path).unwrap(); + let mut vault = File::create(vault_path).unwrap(); vault .write_all("DOTENV_VAULT_PRODUCTION=\"s7NYXa809k/bVSPwIAmJhPJmEGTtU0hG58hOZy7I0ix6y5HP8LsHBsZCYC/gw5DDFy5DgOcyd18R\"".as_bytes()) .unwrap(); @@ -156,7 +156,7 @@ mod tests { fn dotenv_override_fallback_to_env() { let tmp = tempdir().unwrap(); let env_path = tmp.path().join(".env"); - let mut env = File::create(&env_path).unwrap(); + let mut env = File::create(env_path).unwrap(); env.write_all("TESTKEY=\"from .env\"".as_bytes()).unwrap(); env.sync_all().unwrap(); diff --git a/src/log.rs b/src/log.rs index bdd3b0e..49df87c 100644 --- a/src/log.rs +++ b/src/log.rs @@ -7,7 +7,7 @@ macro_rules! log_fn { where T: Display, { - println!( + eprintln!( "[dotenv-vault@{}][{}] {}", env!("CARGO_PKG_VERSION"), $level, diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..db03176 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,110 @@ +use argh::FromArgs; +use std::env; +use std::path::PathBuf; +use std::process::{exit, Command, Stdio}; + +#[derive(FromArgs, PartialEq, Debug)] +/// The CLI program to load the .env.vault file and run the specified program with the specified arguments. +/// +/// You have to set the DOTENV_KEY environment variable before calling dotenv-vault. +/// +/// Example: +/// dotenv-vault run -- my_program arg1 arg2 +struct Opts { + #[argh(subcommand)] + commands: Commands, +} + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand)] +enum Commands { + Run(Run), +} + +#[derive(FromArgs, PartialEq, Debug)] +/// Load the .env.vault file and run the specified program with the specified arguments. +#[argh(subcommand, name = "run")] +struct Run { + #[argh(switch, long = "override")] + /// whether to override the existing environment variables + override_: bool, + + #[argh(option)] + /// current working directory to run the program in + cwd: Option, + + #[argh(positional)] + /// the program to run + program: String, + + #[argh(positional)] + /// the arguments to pass to the program + program_args: Vec, +} + +#[derive(Debug)] +#[repr(i32)] +enum CLIError { + EnvLoad = 1, + EnvOverrideLoad = 2, + ProgramExecution = 3, + CwdChange = 4, +} + +fn main() { + let opts = argh::from_env::(); + + match opts.commands { + Commands::Run(run_opts) => { + let current_cwd = env::current_dir().unwrap(); + + if let Some(given_cwd) = run_opts.cwd { + env::set_current_dir(given_cwd).unwrap_or_else(|err| { + eprintln!("Failed to change the current working directory: {}", err); + exit(CLIError::CwdChange as i32); + }); + } + + // Load the .env.vault file + if run_opts.override_ { + dotenv_vault::dotenv_override().unwrap_or_else(|err| { + eprintln!("Failed to load env: {}", err); + exit(CLIError::EnvOverrideLoad as i32); + }); + } else { + dotenv_vault::dotenv().unwrap_or_else(|err| { + eprintln!("Failed to load env: {}", err); + exit(CLIError::EnvLoad as i32); + }); + }; + + // Run the specified program with the specified arguments + let output = Command::new(&run_opts.program) + .args(run_opts.program_args) + .envs(env::vars()) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .output() + .unwrap_or_else(|err| { + eprintln!("Failed to execute program {}: {}", run_opts.program, err); + exit(CLIError::ProgramExecution as i32); + }); + + // Restore the current working directory + env::set_current_dir(current_cwd).unwrap_or_else(|err| { + eprintln!("Failed to change the current working directory: {}", err); + exit(CLIError::CwdChange as i32); + }); + + if !output.status.success() { + exit( + output + .status + .code() + .unwrap_or(CLIError::ProgramExecution as i32), + ); + } + } + } +} diff --git a/src/vault.rs b/src/vault.rs index e7cad20..bc62eab 100644 --- a/src/vault.rs +++ b/src/vault.rs @@ -153,26 +153,24 @@ impl Vault { }; for key in keys.split(',') { - match self + if let Ok(decrypted) = self .instructions(key) .and_then(|(k, e)| { let vault = dotenvy::from_path_iter(path)?; - let ciphertext = match vault.into_iter().find(|item| match item { + let maybe_ciphertext = vault.into_iter().find(|item| match item { Ok((k, _)) => k == &e, _ => false, - }) { + }); + let ciphertext = match maybe_ciphertext { Some(Ok((_, c))) => c, - _ => { - return Err(Error::EnvironmentNotFound(e)); - } + _ => return Err(Error::EnvironmentNotFound(e)), }; Ok((ciphertext, k)) }) .and_then(|(c, k)| self.decrypt(c, k)) { - Ok(decrypted) => return Ok(decrypted), - Err(_) => continue, + return Ok(decrypted); } } @@ -202,9 +200,8 @@ mod tests { #[test] fn instructions_ok() { let vault = Vault::new(); - let instructions = vault.instructions( - "dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=production".into(), - ); + let instructions = vault + .instructions("dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=production"); assert!(instructions.is_ok()); let (key, environment) = instructions.unwrap(); @@ -215,8 +212,8 @@ mod tests { #[test] fn instructions_invalid_scheme() { let vault = Vault::new(); - let instructions = vault - .instructions("invalid://dotenv.org/vault/.env.vault?environment=production".into()); + let instructions = + vault.instructions("invalid://dotenv.org/vault/.env.vault?environment=production"); assert!(instructions.is_err()); assert!(matches!(instructions.unwrap_err(), Error::InvalidScheme)); @@ -225,8 +222,8 @@ mod tests { #[test] fn instructions_missing_key() { let vault = Vault::new(); - let instructions = vault - .instructions("dotenv://dotenv.org/vault/.env.vault?environment=production".into()); + let instructions = + vault.instructions("dotenv://dotenv.org/vault/.env.vault?environment=production"); assert!(instructions.is_err()); assert!(matches!(instructions.unwrap_err(), Error::MissingKey)); @@ -235,8 +232,7 @@ mod tests { #[test] fn instructions_missing_environment() { let vault = Vault::new(); - let instructions = - vault.instructions("dotenv://:key_1234@dotenv.org/vault/.env.vault".into()); + let instructions = vault.instructions("dotenv://:key_1234@dotenv.org/vault/.env.vault"); assert!(instructions.is_err()); assert!(matches!( diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs new file mode 100644 index 0000000..e324995 --- /dev/null +++ b/tests/cli_tests.rs @@ -0,0 +1,73 @@ +use assert_cmd::Command; +use std::{env, fs::File, io::prelude::*}; +use tempfile::tempdir; + +#[test] +fn dotenv_vault_cli() { + env::set_var("DOTENV_KEY", "dotenv://:key_ddcaa26504cd70a6fef9801901c3981538563a1767c297cb8416e8a38c62fe00@dotenv.local/vault/.env.vault?environment=production"); + + let tmp = tempdir().unwrap(); + let vault_path = tmp.path().join(".env.vault"); + let mut vault = File::create(vault_path).unwrap(); + vault + .write_all("DOTENV_VAULT_PRODUCTION=\"s7NYXa809k/bVSPwIAmJhPJmEGTtU0hG58hOZy7I0ix6y5HP8LsHBsZCYC/gw5DDFy5DgOcyd18R\"".as_bytes()) + .unwrap(); + vault.sync_all().unwrap(); + + let cwd = env::current_dir().unwrap(); + env::set_current_dir(&tmp).unwrap(); + + { + // Run the CLI program with dotenv-vault run -- + let mut cmd = Command::cargo_bin("dotenv-vault").unwrap(); + if cfg!(windows) { + cmd.args(["run", "--", "cmd", "/C", "echo %ALPHA%"]); + } else { + cmd.args(["run", "--", "bash", "-c", "printenv ALPHA"]); + } + + cmd.assert().success(); + let output = cmd.output().unwrap(); + assert_eq!(String::from_utf8(output.stdout).unwrap(), "zeta\n"); + } + + env::set_current_dir(&cwd).unwrap(); + + { + env::set_var("ALPHA", "beta"); + + // override the existing environment variables and specify the current working directory + let mut cmd = Command::cargo_bin("dotenv-vault").unwrap(); + if cfg!(windows) { + cmd.args([ + "run", + "--cwd", + tmp.path().to_string_lossy().as_ref(), + "--override", + "--", + "cmd", + "/C", + "echo %ALPHA%", + ]); + } else { + cmd.args([ + "run", + "--cwd", + tmp.path().to_string_lossy().as_ref(), + "--override", + "--", + "bash", + "-c", + "printenv ALPHA", + ]); + } + + cmd.assert().success(); + let output = cmd.output().unwrap(); + assert_eq!(String::from_utf8(output.stdout).unwrap(), "zeta\n"); + } + + tmp.close().unwrap(); + env::remove_var("DOTENV_KEY"); + env::set_current_dir(cwd).unwrap(); +}