fix: resolve CI workflow path and add lint job
This commit is contained in:
320
src/env_injector.rs
Normal file
320
src/env_injector.rs
Normal file
@@ -0,0 +1,320 @@
|
||||
use crate::vault::Vault;
|
||||
use std::fs;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EnvInjectorError {
|
||||
FileNotFound(String),
|
||||
ReadError(String),
|
||||
WriteError(String),
|
||||
ParseError(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for EnvInjectorError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
EnvInjectorError::FileNotFound(path) => write!(f, "File not found: {}", path),
|
||||
EnvInjectorError::ReadError(msg) => write!(f, "Read error: {}", msg),
|
||||
EnvInjectorError::WriteError(msg) => write!(f, "Write error: {}", msg),
|
||||
EnvInjectorError::ParseError(msg) => write!(f, "Parse error: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for EnvInjectorError {}
|
||||
|
||||
pub struct EnvEntry {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
pub line_number: usize,
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
impl EnvEntry {
|
||||
pub fn from_line(line: &str, line_num: usize) -> Option<Self> {
|
||||
let trimmed = line.trim();
|
||||
|
||||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(eq_pos) = trimmed.find('=') {
|
||||
let key = trimmed[..eq_pos].trim().to_string();
|
||||
let value = trimmed[eq_pos + 1..].trim().to_string();
|
||||
|
||||
Some(EnvEntry {
|
||||
key,
|
||||
value,
|
||||
line_number: line_num,
|
||||
comment: None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format(&self) -> String {
|
||||
format!("{}={}", self.key, self.value)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inject_tokens(
|
||||
vault: &Vault,
|
||||
env_path: &str,
|
||||
prefix: Option<String>,
|
||||
dry_run: bool,
|
||||
) -> Result<(), EnvInjectorError> {
|
||||
let path = Path::new(env_path);
|
||||
|
||||
let existing_entries = if path.exists() {
|
||||
read_env_file(path)?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let prefix = prefix.unwrap_or_else(|| "API_TOKEN_".to_string());
|
||||
|
||||
let mut changes = Vec::new();
|
||||
let mut output_lines: Vec<String> = Vec::new();
|
||||
|
||||
let mut existing_keys: Vec<String> = existing_entries.iter()
|
||||
.map(|e| e.key.clone())
|
||||
.collect();
|
||||
|
||||
let mut lines: Vec<String> = if path.exists() {
|
||||
read_file_lines(path)?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
for (token_name, token_data) in &vault.tokens {
|
||||
let env_key = format!("{}{}", prefix, token_name.to_uppercase());
|
||||
|
||||
let existing_entry = existing_entries.iter()
|
||||
.find(|e| e.key == env_key);
|
||||
|
||||
match existing_entry {
|
||||
Some(entry) => {
|
||||
let new_line = format!("{}={}", env_key, token_data.value);
|
||||
let line_idx = entry.line_number.saturating_sub(1);
|
||||
|
||||
if line_idx < lines.len() && lines[line_idx] != new_line {
|
||||
changes.push((format!("Updated: {}={}", env_key, mask_value(&token_data.value))));
|
||||
if !dry_run {
|
||||
lines[line_idx] = new_line;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
changes.push((format!("Added: {}={}", env_key, mask_value(&token_data.value))));
|
||||
if !dry_run {
|
||||
lines.push(format!("{}={}", env_key, token_data.value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dry_run {
|
||||
if !changes.is_empty() {
|
||||
println!("Dry run - would make {} changes:", changes.len());
|
||||
for change in &changes {
|
||||
println!(" {}", change);
|
||||
}
|
||||
} else {
|
||||
println!("No changes needed.");
|
||||
}
|
||||
} else {
|
||||
let content = lines.join("\n");
|
||||
write_env_file(path, &content)?;
|
||||
|
||||
if !changes.is_empty() {
|
||||
println!("Applied {} changes:", changes.len());
|
||||
for change in &changes {
|
||||
println!(" {}", change);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mask_value(value: &str) -> String {
|
||||
if value.len() <= 4 {
|
||||
"****".to_string()
|
||||
} else {
|
||||
format!("{}****", &value[..4])
|
||||
}
|
||||
}
|
||||
|
||||
fn read_env_file(path: &Path) -> Result<Vec<EnvEntry>, EnvInjectorError> {
|
||||
let content = fs::read_to_string(path)
|
||||
.map_err(|e| EnvInjectorError::ReadError(e.to_string()))?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
|
||||
for (line_num, line) in content.lines().enumerate() {
|
||||
if let Some(entry) = EnvEntry::from_line(line, line_num + 1) {
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
fn read_file_lines(path: &Path) -> Result<Vec<String>, EnvInjectorError> {
|
||||
let content = fs::read_to_string(path)
|
||||
.map_err(|e| EnvInjectorError::ReadError(e.to_string()))?;
|
||||
|
||||
Ok(content.lines().map(String::from).collect())
|
||||
}
|
||||
|
||||
fn write_env_file(path: &Path, content: &str) -> Result<(), EnvInjectorError> {
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(path)
|
||||
.map_err(|e| EnvInjectorError::WriteError(e.to_string()))?;
|
||||
|
||||
file.write_all(content.as_bytes())
|
||||
.map_err(|e| EnvInjectorError::WriteError(e.to_string()))?;
|
||||
|
||||
if !content.is_empty() {
|
||||
file.write_all(b"\n")
|
||||
.map_err(|e| EnvInjectorError::WriteError(e.to_string()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn backup_env_file(path: &Path) -> Result<String, EnvInjectorError> {
|
||||
let backup_path = format!("{}.bak", path.display());
|
||||
fs::copy(path, &backup_path)
|
||||
.map_err(|e| EnvInjectorError::WriteError(e.to_string()))?;
|
||||
Ok(backup_path)
|
||||
}
|
||||
|
||||
pub fn read_env_value(path: &Path, key: &str) -> Result<Option<String>, EnvInjectorError> {
|
||||
let entries = read_env_file(path)?;
|
||||
|
||||
Ok(entries.iter()
|
||||
.find(|e| e.key == key)
|
||||
.map(|e| e.value.clone()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::vault::Vault;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn get_test_env_path() -> PathBuf {
|
||||
PathBuf::from("/tmp/test.env")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_entry_parsing() {
|
||||
let entry = EnvEntry::from_line("API_KEY=my_secret_key", 1);
|
||||
assert!(entry.is_some());
|
||||
let entry = entry.unwrap();
|
||||
assert_eq!(entry.key, "API_KEY");
|
||||
assert_eq!(entry.value, "my_secret_key");
|
||||
assert_eq!(entry.line_number, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_and_comment_lines() {
|
||||
assert!(EnvEntry::from_line("", 1).is_none());
|
||||
assert!(EnvEntry::from_line("# This is a comment", 1).is_none());
|
||||
assert!(EnvEntry::from_line(" ", 1).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inject_into_new_file() {
|
||||
let password = "test";
|
||||
let project = "test_inject";
|
||||
let env_path = get_test_env_path();
|
||||
|
||||
let mut vault = Vault::initialize(password, project).unwrap();
|
||||
vault.generate_token("my_token", 32).unwrap();
|
||||
vault.set_rotation("my_token", 30).unwrap();
|
||||
|
||||
if env_path.exists() {
|
||||
fs::remove_file(&env_path).unwrap();
|
||||
}
|
||||
|
||||
let result = inject_tokens(&vault, env_path.to_str().unwrap(), None, false);
|
||||
assert!(result.is_ok());
|
||||
assert!(env_path.exists());
|
||||
|
||||
let content = fs::read_to_string(&env_path).unwrap();
|
||||
assert!(content.contains("API_TOKEN_MY_TOKEN="));
|
||||
|
||||
fs::remove_file(&env_path).unwrap();
|
||||
|
||||
let vault_path = Vault::get_vault_path(project).unwrap();
|
||||
if vault_path.exists() {
|
||||
fs::remove_file(&vault_path).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inject_into_existing_file() {
|
||||
let password = "test";
|
||||
let project = "test_inject_existing";
|
||||
let env_path = get_test_env_path();
|
||||
|
||||
let mut vault = Vault::initialize(password, project).unwrap();
|
||||
vault.generate_token("new_token", 32).unwrap();
|
||||
vault.set_rotation("new_token", 30).unwrap();
|
||||
|
||||
fs::write(&env_path, "EXISTING_VAR=existing_value\n").unwrap();
|
||||
|
||||
let result = inject_tokens(&vault, env_path.to_str().unwrap(), None, false);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let content = fs::read_to_string(&env_path).unwrap();
|
||||
assert!(content.contains("EXISTING_VAR=existing_value"));
|
||||
assert!(content.contains("API_TOKEN_NEW_TOKEN="));
|
||||
|
||||
fs::remove_file(&env_path).unwrap();
|
||||
|
||||
let vault_path = Vault::get_vault_path(project).unwrap();
|
||||
if vault_path.exists() {
|
||||
fs::remove_file(&vault_path).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dry_run() {
|
||||
let password = "test";
|
||||
let project = "test_dry_run";
|
||||
let env_path = get_test_env_path();
|
||||
|
||||
let mut vault = Vault::initialize(password, project).unwrap();
|
||||
vault.generate_token("dry_token", 32).unwrap();
|
||||
vault.set_rotation("dry_token", 30).unwrap();
|
||||
|
||||
if env_path.exists() {
|
||||
fs::remove_file(&env_path).unwrap();
|
||||
}
|
||||
|
||||
fs::write(&env_path, "# Existing file").unwrap();
|
||||
let original_content = fs::read_to_string(&env_path).unwrap();
|
||||
|
||||
let result = inject_tokens(&vault, env_path.to_str().unwrap(), None, true);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let new_content = fs::read_to_string(&env_path).unwrap();
|
||||
assert_eq!(original_content, new_content);
|
||||
|
||||
fs::remove_file(&env_path).unwrap();
|
||||
|
||||
let vault_path = Vault::get_vault_path(project).unwrap();
|
||||
if vault_path.exists() {
|
||||
fs::remove_file(&vault_path).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user