This commit is contained in:
2025-04-02 08:24:03 +02:00
parent 3606e27e30
commit b2896b206c
11 changed files with 739 additions and 15 deletions

View File

@@ -1,7 +1,355 @@
use std::process::Command;
use std::path::Path;
use std::fs;
use std::env;
use regex::Regex;
use std::fmt;
use std::process::{Command, Output};
use std::error::Error;
use std::fmt;
use std::collections::HashMap;
use redis::Cmd;
use serde::{Deserialize, Serialize};
use crate::env::redisclient;
use crate::git::git::parse_git_url;
// Define a custom error type for GitExecutor operations
#[derive(Debug)]
pub enum GitExecutorError {
GitCommandFailed(String),
CommandExecutionError(std::io::Error),
RedisError(redis::RedisError),
JsonError(serde_json::Error),
AuthenticationError(String),
SshAgentNotLoaded,
InvalidAuthConfig(String),
}
// Implement Display for GitExecutorError
impl fmt::Display for GitExecutorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GitExecutorError::GitCommandFailed(e) => write!(f, "Git command failed: {}", e),
GitExecutorError::CommandExecutionError(e) => write!(f, "Command execution error: {}", e),
GitExecutorError::RedisError(e) => write!(f, "Redis error: {}", e),
GitExecutorError::JsonError(e) => write!(f, "JSON error: {}", e),
GitExecutorError::AuthenticationError(e) => write!(f, "Authentication error: {}", e),
GitExecutorError::SshAgentNotLoaded => write!(f, "SSH agent is not loaded"),
GitExecutorError::InvalidAuthConfig(e) => write!(f, "Invalid authentication configuration: {}", e),
}
}
}
// Implement Error trait for GitExecutorError
impl Error for GitExecutorError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
GitExecutorError::CommandExecutionError(e) => Some(e),
GitExecutorError::RedisError(e) => Some(e),
GitExecutorError::JsonError(e) => Some(e),
_ => None,
}
}
}
// From implementations for error conversion
impl From<redis::RedisError> for GitExecutorError {
fn from(err: redis::RedisError) -> Self {
GitExecutorError::RedisError(err)
}
}
impl From<serde_json::Error> for GitExecutorError {
fn from(err: serde_json::Error) -> Self {
GitExecutorError::JsonError(err)
}
}
impl From<std::io::Error> for GitExecutorError {
fn from(err: std::io::Error) -> Self {
GitExecutorError::CommandExecutionError(err)
}
}
// Status enum for GitConfig
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub enum GitConfigStatus {
#[serde(rename = "error")]
Error,
#[serde(rename = "ok")]
Ok,
}
// Auth configuration for a specific git server
#[derive(Debug, Serialize, Deserialize)]
pub struct GitServerAuth {
pub sshagent: Option<bool>,
pub key: Option<String>,
pub username: Option<String>,
pub password: Option<String>,
}
// Main configuration structure from Redis
#[derive(Debug, Serialize, Deserialize)]
pub struct GitConfig {
pub status: GitConfigStatus,
pub auth: HashMap<String, GitServerAuth>,
}
// GitExecutor struct
pub struct GitExecutor {
config: Option<GitConfig>,
}
impl GitExecutor {
// Create a new GitExecutor
pub fn new() -> Self {
GitExecutor { config: None }
}
// Initialize by loading configuration from Redis
pub fn init(&mut self) -> Result<(), GitExecutorError> {
// Try to load config from Redis
match self.load_config_from_redis() {
Ok(config) => {
self.config = Some(config);
Ok(())
}
Err(e) => {
// If Redis error, we'll proceed without config
// This is not a fatal error as we might use default git behavior
eprintln!("Warning: Failed to load git config from Redis: {}", e);
self.config = None;
Ok(())
}
}
}
// Load configuration from Redis
fn load_config_from_redis(&self) -> Result<GitConfig, GitExecutorError> {
// Create Redis command to get the herocontext:git key
let mut cmd = Cmd::new();
cmd.arg("GET").arg("herocontext:git");
// Execute the command
let result: redis::RedisResult<String> = redisclient::execute(&mut cmd);
match result {
Ok(json_str) => {
// Parse the JSON string into GitConfig
let config: GitConfig = serde_json::from_str(&json_str)?;
// Validate the config
if config.status == GitConfigStatus::Error {
return Err(GitExecutorError::InvalidAuthConfig("Config status is error".to_string()));
}
Ok(config)
}
Err(e) => Err(GitExecutorError::RedisError(e)),
}
}
// Check if SSH agent is loaded
fn is_ssh_agent_loaded(&self) -> bool {
let output = Command::new("ssh-add")
.arg("-l")
.output();
match output {
Ok(output) => output.status.success() && !output.stdout.is_empty(),
Err(_) => false,
}
}
// Get authentication configuration for a git URL
fn get_auth_for_url(&self, url: &str) -> Option<&GitServerAuth> {
if let Some(config) = &self.config {
let (server, _, _) = parse_git_url(url);
if !server.is_empty() {
return config.auth.get(&server);
}
}
None
}
// Validate authentication configuration
fn validate_auth_config(&self, auth: &GitServerAuth) -> Result<(), GitExecutorError> {
// Rule: If sshagent is true, other fields should be empty
if let Some(true) = auth.sshagent {
if auth.key.is_some() || auth.username.is_some() || auth.password.is_some() {
return Err(GitExecutorError::InvalidAuthConfig(
"When sshagent is true, key, username, and password must be empty".to_string()
));
}
// Check if SSH agent is actually loaded
if !self.is_ssh_agent_loaded() {
return Err(GitExecutorError::SshAgentNotLoaded);
}
}
// Rule: If key is set, other fields should be empty
if let Some(_) = &auth.key {
if auth.sshagent.unwrap_or(false) || auth.username.is_some() || auth.password.is_some() {
return Err(GitExecutorError::InvalidAuthConfig(
"When key is set, sshagent, username, and password must be empty".to_string()
));
}
}
// Rule: If username is set, password should be set and other fields empty
if let Some(_) = &auth.username {
if auth.sshagent.unwrap_or(false) || auth.key.is_some() {
return Err(GitExecutorError::InvalidAuthConfig(
"When username is set, sshagent and key must be empty".to_string()
));
}
if auth.password.is_none() {
return Err(GitExecutorError::InvalidAuthConfig(
"When username is set, password must also be set".to_string()
));
}
}
Ok(())
}
// Execute a git command with authentication
pub fn execute(&self, args: &[&str]) -> Result<Output, GitExecutorError> {
// Extract the git URL if this is a command that needs authentication
let url_arg = self.extract_git_url_from_args(args);
// If we have a URL and authentication config, use it
if let Some(url) = url_arg {
if let Some(auth) = self.get_auth_for_url(&url) {
// Validate the authentication configuration
self.validate_auth_config(auth)?;
// Execute with the appropriate authentication method
return self.execute_with_auth(args, auth);
}
}
// No special authentication needed, execute normally
self.execute_git_command(args)
}
// Extract git URL from command arguments
fn extract_git_url_from_args<'a>(&self, args: &[&'a str]) -> Option<&'a str> {
// Commands that might contain a git URL
if args.contains(&"clone") || args.contains(&"fetch") || args.contains(&"pull") || args.contains(&"push") {
// The URL is typically the last argument for clone, or after remote for others
for (i, &arg) in args.iter().enumerate() {
if arg == "clone" && i + 1 < args.len() {
return Some(args[i + 1]);
}
if (arg == "fetch" || arg == "pull" || arg == "push") && i + 1 < args.len() {
// For these commands, the URL might be specified as a remote name
// We'd need more complex logic to resolve remote names to URLs
// For now, we'll just return None
return None;
}
}
}
None
}
// Execute git command with authentication
fn execute_with_auth(&self, args: &[&str], auth: &GitServerAuth) -> Result<Output, GitExecutorError> {
// Handle different authentication methods
if let Some(true) = auth.sshagent {
// Use SSH agent (already validated that it's loaded)
self.execute_git_command(args)
} else if let Some(key) = &auth.key {
// Use SSH key
self.execute_with_ssh_key(args, key)
} else if let Some(username) = &auth.username {
// Use username/password
if let Some(password) = &auth.password {
self.execute_with_credentials(args, username, password)
} else {
// This should never happen due to validation
Err(GitExecutorError::AuthenticationError("Password is required when username is set".to_string()))
}
} else {
// No authentication method specified, use default
self.execute_git_command(args)
}
}
// Execute git command with SSH key
fn execute_with_ssh_key(&self, args: &[&str], key: &str) -> Result<Output, GitExecutorError> {
// Create a command with GIT_SSH_COMMAND to specify the key
let ssh_command = format!("ssh -i {} -o IdentitiesOnly=yes", key);
let mut command = Command::new("git");
command.env("GIT_SSH_COMMAND", ssh_command);
command.args(args);
let output = command.output()?;
if output.status.success() {
Ok(output)
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(GitExecutorError::GitCommandFailed(error.to_string()))
}
}
// Execute git command with username/password
fn execute_with_credentials(&self, args: &[&str], username: &str, password: &str) -> Result<Output, GitExecutorError> {
// Helper method to execute a command and handle the result
fn execute_command(command: &mut Command) -> Result<Output, GitExecutorError> {
let output = command.output()?;
if output.status.success() {
Ok(output)
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(GitExecutorError::GitCommandFailed(error.to_string()))
}
}
// For HTTPS authentication, we need to modify the URL to include credentials
// Create a new vector to hold our modified arguments
let modified_args: Vec<String> = args.iter().map(|&arg| {
if arg.starts_with("https://") {
// Replace https:// with https://username:password@
format!("https://{}:{}@{}",
username,
password,
&arg[8..]) // Skip the "https://" part
} else {
arg.to_string()
}
}).collect();
// Execute the command
let mut command = Command::new("git");
// Add the modified arguments to the command
for arg in &modified_args {
command.arg(arg.as_str());
}
// Execute the command and handle the result
let output = command.output()?;
if output.status.success() { Ok(output) } else { Err(GitExecutorError::GitCommandFailed(String::from_utf8_lossy(&output.stderr).to_string())) }
}
// Basic git command execution
fn execute_git_command(&self, args: &[&str]) -> Result<Output, GitExecutorError> {
let mut command = Command::new("git");
command.args(args);
let output = command.output()?;
if output.status.success() {
Ok(output)
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(GitExecutorError::GitCommandFailed(error.to_string()))
}
}
}
// Implement Default for GitExecutor
impl Default for GitExecutor {
fn default() -> Self {
Self::new()
}
}

View File

@@ -1,12 +1,32 @@
in @/sal/git/git_executor.rs
create a GitExecutor which is the one executing git commands
and also checking if ssh-agent is loaded
or if there is an authentication mechanism defined in redis
check if there is a redis on ~/hero/var/redis.sock
how is this done use src/env/redisclient.rs
this allows us to execute redis commands
over unix domani socket
check there is herocontext:git in the redis
if yes then check there is an entry on
if yes fetch the object, its a json representing a struct
sal::git::
- status (error, ok) as enum
- auth which is a map
- key is the server part of our git url (see parse_git_url in git module)
- val is another object with following properties
- sshagent as bool (means if set just use loaded sshagent)
- key (is the sshkey as needs to be used when talking to the server)
- username (if username then there needs to be a password)
- password
we need to deserialize this struct
this now tells based on the server name how to authenticate for the git server
if sshagent then rest needs to be empty
if key rest needs to be empty
if username then password set, rest empty
the git executor needs to use above to talk in right way to the server

View File

@@ -1,2 +1,6 @@
use git::*;
use git_executor::*;
mod git;
mod git_executor;
pub use git::*;
pub use git_executor::*;